refactor secubox app packaging and theme

This commit is contained in:
CyberMind-FR 2025-12-29 21:57:12 +01:00
parent 06ac101e5a
commit 92eff5aad7
53 changed files with 3270 additions and 284 deletions

View File

@ -219,7 +219,7 @@ Pointer: see `docs/embedded/docker-zigbee2mqtt.md` for the canonical version.
Pointer: see `docs/embedded/vhost-manager.md` for the canonical version.
#### **embedded/app-store.md** 🛒
*Plugin manifest format and CLI for the SecuBox App Store.*
*Manifest schema, `secubox-app` CLI usage, and packaged SecuBox apps (Zigbee2MQTT, Lyrion, Domoticz).*
Pointer: see `docs/embedded/app-store.md` for the canonical version.

View File

@ -4,7 +4,7 @@
**Last Updated:** 2025-12-28
**Status:** Active
This guide outlines the initial “SecuBox Apps” registry format and the `secubox-app` CLI helper. It currently ships with a single manifest (Zigbee2MQTT), but the workflow scales to other Docker/LXC/native services such as Lyrion.
This guide outlines the “SecuBox Apps” registry format and the `secubox-app` CLI helper. The App Store currently ships manifests for Zigbee2MQTT, Lyrion Media Server, and Domoticz, with the workflow ready for additional Docker/LXC/native services.
---
@ -65,7 +65,7 @@ Each plugin folder contains a `manifest.json`. Example (Zigbee2MQTT):
## CLI Usage (`secubox-app`)
`luci-app-secubox` installs the CLI as `/usr/sbin/secubox-app` (also available under `secubox-tools/` for development). Commands:
`secubox-app` is shipped as a standalone OpenWrt package (see `package/secubox/secubox-app`) and installs the CLI at `/usr/sbin/secubox-app`. Commands:
```bash
# List manifests
@ -92,6 +92,20 @@ The CLI relies on `opkg` and `jsonfilter`, so run it on the router (or within th
---
## Packaged SecuBox Apps
`secubox-app-*` packages provide the runtime pieces behind each manifest (init scripts, helpers, and default configs). They are copied automatically by `secubox-tools/local-build.sh` into both firmware builds and the SDK feed, so developers get the same artifacts as the LuCI wizard and CLI.
| Package | Manifest ID | Purpose |
|---------|-------------|---------|
| `secubox-app-zigbee2mqtt` | `zigbee2mqtt` | Installs Docker runner + `zigbee2mqttctl`, exposes splash/log helpers, and ships default UCI config. |
| `secubox-app-lyrion` | `lyrion` | Deploys the Lyrion Media Server container, CLI (`lyrionctl`), and profile hooks for HTTPS publishing. |
| `secubox-app-domoticz` | `domoticz` | Provides Domoticz Docker automation (`domoticzctl`) and the base data/service layout consumed by the wizard. |
All three packages declare their dependencies (Docker, vhost manager, etc.) so `secubox-app install <id>` only has to orchestrate actions, not guess at required feeds.
---
## Future Integration
- LuCI App Store page will consume the same manifest directory to render cards, filters, and install buttons.

View File

@ -219,7 +219,7 @@ Pointer: see `docs/embedded/docker-zigbee2mqtt.md` for the canonical version.
Pointer: see `docs/embedded/vhost-manager.md` for the canonical version.
#### **embedded/app-store.md** 🛒
*Plugin manifest format and CLI for the SecuBox App Store.*
*Manifest schema, `secubox-app` CLI usage, and packaged SecuBox apps (Zigbee2MQTT, Lyrion, Domoticz).*
Pointer: see `docs/embedded/app-store.md` for the canonical version.

View File

@ -24,19 +24,10 @@ include $(TOPDIR)/feeds/luci/luci.mk
define Package/$(PKG_NAME)/install
$(call Package/luci/install,$(1))
$(INSTALL_DIR) $(1)/usr/share/secubox/plugins
for dir in $(CURDIR)/../plugins/*; do \
[ -d $$dir ] || continue; \
name=$$(basename $$dir); \
$(INSTALL_DIR) $(1)/usr/share/secubox/plugins/$$name; \
$(INSTALL_DATA) $$dir/manifest.json $(1)/usr/share/secubox/plugins/$$name/manifest.json; \
done
$(INSTALL_DIR) $(1)/usr/share/secubox/profiles
for file in $(CURDIR)/../profiles/*.json; do \
for file in $(CURDIR)/profiles/*.json; do \
$(INSTALL_DATA) $$file $(1)/usr/share/secubox/profiles/$$(basename $$file); \
done
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(CURDIR)/../secubox-tools/secubox-app $(1)/usr/sbin/secubox-app
endef
# call BuildPackage - OpenWrt buildroot

View File

@ -0,0 +1,18 @@
'use strict';
return {
stylesheet: function(name) {
var primary = L.resource('secubox-theme/system-hub/' + name);
var fallback = L.resource('system-hub/' + name);
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = primary;
link.onerror = function() {
if (link.dataset.fallbackApplied) return;
link.dataset.fallbackApplied = '1';
console.warn('System Hub theme asset missing:', primary, 'falling back to', fallback);
link.href = fallback;
};
return link;
}
};

View File

@ -3,6 +3,7 @@
'require ui';
'require system-hub/api as API';
'require secubox-theme/theme as Theme';
'require system-hub/theme-assets as ThemeAssets';
'require system-hub/nav as HubNav';
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
@ -23,9 +24,9 @@ return view.extend({
render: function() {
return E('div', { 'class': 'system-hub-dashboard sh-backup-view' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/backup.css') }),
ThemeAssets.stylesheet('common.css'),
ThemeAssets.stylesheet('dashboard.css'),
ThemeAssets.stylesheet('backup.css'),
HubNav.renderTabs('backup'),
this.renderHeader(),
this.renderHero(),

View File

@ -5,6 +5,7 @@
'require poll';
'require system-hub/api as API';
'require secubox-theme/theme as Theme';
'require system-hub/theme-assets as ThemeAssets';
'require system-hub/nav as HubNav';
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
@ -27,9 +28,9 @@ return view.extend({
var view = E('div', { 'class': 'system-hub-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/components.css') }),
ThemeAssets.stylesheet('common.css'),
ThemeAssets.stylesheet('dashboard.css'),
ThemeAssets.stylesheet('components.css'),
HubNav.renderTabs('components'),

View File

@ -1,6 +1,7 @@
'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require system-hub/theme-assets as ThemeAssets';
'require system-hub/dev-status-widget as DevStatusWidget';
'require system-hub/nav as HubNav';
@ -24,8 +25,8 @@ return view.extend({
var widget = this.getWidget();
var container = E('div', { 'class': 'system-hub-dev-status' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
ThemeAssets.stylesheet('common.css'),
ThemeAssets.stylesheet('dashboard.css'),
HubNav.renderTabs('dev-status'),
this.renderHeader(),
this.renderSummaryGrid(),

View File

@ -5,6 +5,7 @@
'require fs';
'require secubox-theme/theme as Theme';
'require system-hub/api as API';
'require system-hub/theme-assets as ThemeAssets';
'require system-hub/nav as HubNav';
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
@ -23,8 +24,8 @@ return view.extend({
var view = E('div', { 'class': 'system-hub-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
ThemeAssets.stylesheet('common.css'),
ThemeAssets.stylesheet('dashboard.css'),
HubNav.renderTabs('diagnostics'),
// Collect Diagnostics

View File

@ -5,6 +5,7 @@
'require poll';
'require system-hub/api as API';
'require secubox-theme/theme as Theme';
'require system-hub/theme-assets as ThemeAssets';
'require system-hub/nav as HubNav';
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
@ -24,9 +25,9 @@ return view.extend({
var container = E('div', { 'class': 'system-hub-dashboard sh-health-view' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/health.css') }),
ThemeAssets.stylesheet('common.css'),
ThemeAssets.stylesheet('dashboard.css'),
ThemeAssets.stylesheet('health.css'),
HubNav.renderTabs('health'),
this.renderHero(),
this.renderMetricGrid(),

View File

@ -5,6 +5,7 @@
'require poll';
'require system-hub/api as API';
'require secubox-theme/theme as Theme';
'require system-hub/theme-assets as ThemeAssets';
'require system-hub/nav as HubNav';
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
@ -29,9 +30,9 @@ return view.extend({
var container = E('div', { 'class': 'sh-logs-view' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/logs.css') }),
ThemeAssets.stylesheet('common.css'),
ThemeAssets.stylesheet('dashboard.css'),
ThemeAssets.stylesheet('logs.css'),
HubNav.renderTabs('logs'),
this.renderHero(),
this.renderControls(),

View File

@ -5,6 +5,7 @@
'require poll';
'require system-hub/api as API';
'require secubox-theme/theme as Theme';
'require system-hub/theme-assets as ThemeAssets';
'require system-hub/nav as HubNav';
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
@ -29,9 +30,9 @@ return view.extend({
var container = E('div', { 'class': 'sh-overview' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/overview.css') }),
ThemeAssets.stylesheet('common.css'),
ThemeAssets.stylesheet('dashboard.css'),
ThemeAssets.stylesheet('overview.css'),
HubNav.renderTabs('overview'),
this.renderPageHeader(),
this.renderInfoGrid(),

View File

@ -4,6 +4,7 @@
'require ui';
'require secubox-theme/theme as Theme';
'require system-hub/api as API';
'require system-hub/theme-assets as ThemeAssets';
'require system-hub/nav as HubNav';
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
@ -21,8 +22,8 @@ return view.extend({
var view = E('div', { 'class': 'system-hub-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
ThemeAssets.stylesheet('common.css'),
ThemeAssets.stylesheet('dashboard.css'),
HubNav.renderTabs('remote'),
// RustDesk Section

View File

@ -5,6 +5,7 @@
'require poll';
'require system-hub/api as API';
'require secubox-theme/theme as Theme';
'require system-hub/theme-assets as ThemeAssets';
'require system-hub/nav as HubNav';
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
@ -26,9 +27,9 @@ return view.extend({
var container = E('div', { 'class': 'sh-services-view' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/services.css') }),
ThemeAssets.stylesheet('common.css'),
ThemeAssets.stylesheet('dashboard.css'),
ThemeAssets.stylesheet('services.css'),
HubNav.renderTabs('services'),
this.renderHeader(),
this.renderControls(),

View File

@ -3,6 +3,7 @@
'require ui';
'require system-hub/api as API';
'require secubox-theme/theme as Theme';
'require system-hub/theme-assets as ThemeAssets';
'require system-hub/nav as HubNav';
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
@ -24,8 +25,8 @@ return view.extend({
var container = E('div', { 'class': 'system-hub-dashboard sh-settings-view' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
ThemeAssets.stylesheet('common.css'),
ThemeAssets.stylesheet('dashboard.css'),
HubNav.renderTabs('settings'),
this.renderHeader(),
this.renderGeneralSection(),

View File

@ -0,0 +1,100 @@
.sh-backup-view {
padding: 28px;
}
.sh-backup-hero {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
padding: 24px;
border-radius: 20px;
background: var(--sh-gradient-soft);
border: 1px solid var(--sh-border);
box-shadow: var(--sh-shadow);
margin-bottom: 24px;
}
.sh-hero-eyebrow {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.2em;
color: var(--sh-text-secondary);
margin-bottom: 6px;
display: inline-block;
}
.sh-backup-hero h1 {
margin: 0 0 6px;
font-size: 26px;
}
.sh-backup-hero p {
margin: 0;
color: var(--sh-text-secondary);
}
.sh-hero-badges {
display: flex;
gap: 12px;
align-items: center;
}
.sh-hero-badge {
padding: 12px 18px;
border-radius: 16px;
background: rgba(15,23,42,0.5);
border: 1px solid rgba(255,255,255,0.08);
min-width: 140px;
}
.sh-hero-badge .label {
display: block;
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--sh-text-secondary);
}
.sh-hero-badge strong {
font-size: 18px;
}
.sh-backup-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px;
}
.sh-text-muted {
color: var(--sh-text-secondary);
font-size: 14px;
line-height: 1.6;
}
.sh-upload {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 16px;
margin: 16px 0;
border: 1px dashed var(--sh-border);
border-radius: 14px;
color: var(--sh-text-secondary);
cursor: pointer;
transition: all 0.2s;
}
.sh-upload:hover {
background: rgba(255,255,255,0.04);
}
.sh-upload input {
display: none;
}
.sh-action-row {
display: flex;
justify-content: flex-end;
}

View File

@ -0,0 +1,636 @@
/**
* System Hub - Common Styles (Demo-inspired)
* Shared styles across all System Hub pages
* Version: 0.3.0 - Matching https://cybermind.fr/apps/system-hub/demo.html
*/
/* === Import Fonts === */
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap');
/* === Variables (Demo-inspired Dark Mode) === */
:root,
[data-secubox-theme="light"] {
/* Light Mode (less used) */
--sh-text-primary: #0f172a;
--sh-text-secondary: #475569;
--sh-bg-primary: #ffffff;
--sh-bg-secondary: #f8fafc;
--sh-bg-tertiary: #f1f5f9;
--sh-bg-card: #ffffff;
--sh-border: #e2e8f0;
--sh-hover-bg: #f8fafc;
--sh-hover-shadow: rgba(0, 0, 0, 0.1);
--sh-primary: #6366f1;
--sh-primary-end: #8b5cf6;
--sh-shadow: rgba(0, 0, 0, 0.08);
--sh-success: #22c55e;
--sh-danger: #ef4444;
--sh-warning: #f59e0b;
}
[data-theme="dark"],
[data-secubox-theme="dark"] {
/* Demo-inspired Dark Palette */
--sh-text-primary: #fafafa;
--sh-text-secondary: #a0a0b0;
--sh-bg-primary: #0a0a0f;
--sh-bg-secondary: #12121a;
--sh-bg-tertiary: #1a1a24;
--sh-bg-card: #12121a;
--sh-border: #2a2a35;
--sh-hover-bg: #1a1a24;
--sh-hover-shadow: rgba(0, 0, 0, 0.6);
--sh-primary: #6366f1;
--sh-primary-end: #8b5cf6;
--sh-shadow: rgba(0, 0, 0, 0.4);
--sh-success: #22c55e;
--sh-danger: #ef4444;
--sh-warning: #f59e0b;
}
/* === Global Typography === */
body,
.system-hub-dashboard,
.sh-page-header,
.sh-card {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
code,
.sh-mono,
.sh-id-display,
pre {
font-family: 'JetBrains Mono', 'Courier New', monospace;
}
/* === Page Header === */
.sh-page-header {
margin-bottom: 24px;
padding: 24px;
background: var(--sh-bg-card);
border-radius: 16px;
border: 1px solid var(--sh-border);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.sh-page-title {
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: flex;
align-items: center;
gap: 12px;
}
.sh-page-title-icon {
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}
.sh-page-subtitle {
margin: 4px 0 0 0;
font-size: 14px;
color: var(--sh-text-secondary);
font-weight: 500;
}
/* === Stats Badges (Compact Demo Style) === */
.sh-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 12px;
margin: 20px 0;
}
.sh-stat-badge {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 12px;
background: var(--sh-bg-card);
border: 1px solid var(--sh-border);
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.sh-stat-badge::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--sh-primary), var(--sh-primary-end));
opacity: 0;
transition: opacity 0.3s ease;
}
.sh-stat-badge:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px var(--sh-shadow);
border-color: var(--sh-primary);
}
.sh-stat-badge:hover::before {
opacity: 1;
}
.sh-stat-value {
font-size: 28px;
font-weight: 700;
line-height: 1;
margin-bottom: 6px;
color: var(--sh-text-primary);
font-family: 'JetBrains Mono', monospace;
}
.sh-stat-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--sh-text-secondary);
}
/* === Navigation Tabs (Internal Demo Style) === */
.sh-nav-tabs {
display: flex;
gap: 8px;
background: var(--sh-bg-secondary);
padding: 8px;
border-radius: 12px;
border: 1px solid var(--sh-border);
margin-bottom: 24px;
overflow-x: auto;
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(10px);
}
.sh-nav-tab {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
background: transparent;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
font-weight: 600;
color: var(--sh-text-secondary);
white-space: nowrap;
position: relative;
}
.sh-nav-tab:hover {
background: var(--sh-hover-bg);
color: var(--sh-text-primary);
}
/* Hide default LuCI tabs (we render SecuNav instead) */
body[data-page^="admin-secubox-system-system-hub"] .tabs,
body[data-page^="admin-secubox-system-system-hub"] #tabmenu,
body[data-page^="admin-secubox-system-system-hub"] .cbi-tabmenu,
body[data-page^="admin-secubox-system-system-hub"] .nav-tabs {
display: none !important;
}
.sh-nav-tab.active {
color: var(--sh-primary);
background: rgba(99, 102, 241, 0.1);
}
.sh-nav-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 10%;
right: 10%;
height: 2px;
background: linear-gradient(90deg, var(--sh-primary), var(--sh-primary-end));
border-radius: 2px;
}
/* === Filter Tabs === */
.sh-service-tabs,
.sh-component-tabs {
margin-bottom: 24px;
}
/* === Cards (with colored top border) === */
.sh-card {
background: var(--sh-bg-card);
border-radius: 16px;
border: 1px solid var(--sh-border);
overflow: hidden;
transition: all 0.3s ease;
margin-bottom: 20px;
position: relative;
}
.sh-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--sh-primary), var(--sh-primary-end));
opacity: 0;
transition: opacity 0.3s ease;
}
.sh-card:hover {
transform: translateY(-3px);
box-shadow: 0 12px 28px var(--sh-hover-shadow);
border-color: var(--sh-border);
}
.sh-card:hover::before {
opacity: 1;
}
/* Colored borders for different card types */
.sh-card-success::before {
background: var(--sh-success);
opacity: 1;
}
.sh-card-danger::before {
background: var(--sh-danger);
opacity: 1;
}
.sh-card-warning::before {
background: var(--sh-warning);
opacity: 1;
}
.sh-card-header {
padding: 20px 24px;
background: var(--sh-bg-secondary);
border-bottom: 1px solid var(--sh-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.sh-card-title {
font-size: 18px;
font-weight: 700;
color: var(--sh-text-primary);
display: flex;
align-items: center;
gap: 10px;
margin: 0;
}
.sh-card-title-icon {
font-size: 22px;
line-height: 1;
}
.sh-card-badge {
padding: 6px 14px;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
color: #ffffff;
border-radius: 20px;
font-size: 12px;
font-weight: 700;
}
.sh-card-body {
padding: 24px;
}
/* === Empty State === */
.sh-empty-state {
text-align: center;
padding: 60px 20px;
background: var(--sh-bg-secondary);
border-radius: 12px;
border: 2px dashed var(--sh-border);
}
.sh-empty-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
.sh-empty-text {
font-size: 16px;
color: var(--sh-text-secondary);
font-weight: 600;
}
/* === Buttons (Gradient Style) === */
.sh-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
border: none;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.sh-btn-primary {
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
color: #ffffff;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
.sh-btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.5);
}
.sh-btn-success {
background: var(--sh-success);
color: #ffffff;
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.3);
}
.sh-btn-success:hover {
background: #16a34a;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(34, 197, 94, 0.5);
}
.sh-btn-danger {
background: var(--sh-danger);
color: #ffffff;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
.sh-btn-danger:hover {
background: #dc2626;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(239, 68, 68, 0.5);
}
.sh-btn-warning {
background: var(--sh-warning);
color: #ffffff;
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
}
.sh-btn-warning:hover {
background: #d97706;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(245, 158, 11, 0.5);
}
.sh-btn-secondary {
background: var(--sh-bg-tertiary);
color: var(--sh-text-primary);
border: 1px solid var(--sh-border);
}
.sh-btn-secondary:hover {
background: var(--sh-hover-bg);
border-color: var(--sh-primary);
transform: translateY(-2px);
}
/* === ID Display (Monospace) === */
.sh-id-display {
font-size: 24px;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
color: var(--sh-text-primary);
background: var(--sh-bg-secondary);
padding: 12px 20px;
border-radius: 8px;
border: 1px solid var(--sh-border);
text-align: center;
letter-spacing: 2px;
}
/* === Gradient Text Utility === */
.sh-gradient-text {
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* === Responsive === */
@media (max-width: 768px) {
.sh-page-title {
font-size: 22px;
}
.sh-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.sh-nav-tabs {
gap: 4px;
padding: 6px;
}
.sh-nav-tab {
padding: 8px 14px;
font-size: 13px;
}
/* === Development Status Bonus Tab === */
.sh-page-insight {
display: flex;
flex-direction: column;
align-items: flex-end;
text-align: right;
background: var(--sh-bg-card);
border: 1px solid var(--sh-border);
padding: 16px 20px;
border-radius: 12px;
min-width: 220px;
}
.sh-page-insight-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.6px;
color: var(--sh-text-secondary);
font-weight: 600;
}
.sh-page-insight-value {
font-size: 18px;
font-weight: 700;
color: var(--sh-text-primary);
margin: 6px 0 4px;
}
.sh-page-insight-sub {
font-size: 13px;
color: var(--sh-primary);
font-family: 'JetBrains Mono', monospace;
}
.sh-dev-status-grid .sh-stat-badge {
min-height: 110px;
}
.sh-dev-status-widget-shell {
margin-top: 12px;
}
.sh-dev-status-widget-shell #dev-status-widget {
margin-top: 8px;
}
.sh-dev-status-note {
margin-top: 20px;
padding: 14px 16px;
border-radius: 12px;
background: var(--sh-bg-secondary);
border: 1px dashed var(--sh-border);
font-size: 13px;
color: var(--sh-text-secondary);
display: flex;
align-items: center;
gap: 8px;
}
}
/* === Dark Mode Overrides === */
[data-theme="dark"] .sh-card,
[data-theme="dark"] .sh-stat-badge,
[data-theme="dark"] .sh-nav-tabs,
[data-theme="dark"] .sh-page-header {
background: var(--sh-bg-card);
border-color: var(--sh-border);
}
[data-theme="dark"] .sh-card-header,
[data-theme="dark"] .sh-nav-tabs {
background: var(--sh-bg-secondary);
border-color: var(--sh-border);
}
[data-theme="dark"] .sh-nav-tab {
background: transparent;
}
[data-theme="dark"] .sh-nav-tab:hover {
background: var(--sh-hover-bg);
}
[data-theme="dark"] .sh-btn-secondary {
background: var(--sh-bg-tertiary);
border-color: var(--sh-border);
color: var(--sh-text-primary);
}
[data-theme="dark"] .sh-btn-secondary:hover {
background: var(--sh-hover-bg);
}
[data-theme="dark"] .sh-empty-state {
background: var(--sh-bg-secondary);
border-color: var(--sh-border);
}
[data-theme="dark"] .sh-id-display {
background: var(--sh-bg-secondary);
border-color: var(--sh-border);
}
/* Slim header utility */
.sh-page-header-lite {
background: #f7f9fc;
border: 1px solid rgba(148, 163, 184, 0.35);
border-radius: 18px;
padding: 14px 20px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05);
}
.sh-header-meta {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.sh-header-chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 999px;
background: #ffffff;
border: 1px solid rgba(148, 163, 184, 0.3);
font-size: 13px;
font-weight: 600;
color: var(--sh-text-secondary);
}
.sh-header-chip strong {
display: block;
color: var(--sh-text-primary);
font-size: 14px;
}
.sh-chip-text {
line-height: 1.1;
}
.sh-chip-label {
display: block;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--sh-text-muted, #94a3b8);
}
.sh-chip-icon {
font-size: 16px;
}
.sh-header-chip.success {
background: rgba(34, 197, 94, 0.12);
border-color: rgba(34, 197, 94, 0.4);
color: #15803d;
}
.sh-header-chip.danger {
background: rgba(239, 68, 68, 0.12);
border-color: rgba(239, 68, 68, 0.45);
color: #b91c1c;
}
.sh-header-chip.warn {
background: rgba(245, 158, 11, 0.12);
border-color: rgba(245, 158, 11, 0.45);
color: #b45309;
}

View File

@ -0,0 +1,280 @@
/**
* System Hub - Components Page Styles
* Responsive card layout with theme support
* Version: 0.2.2
*/
/* === Header & Filters === */
.sh-components-header {
margin-bottom: 24px;
}
.sh-page-title {
font-size: 28px;
font-weight: 700;
margin: 0 0 20px 0;
color: var(--sh-text-primary, #1e293b);
display: flex;
align-items: center;
gap: 12px;
}
.sh-title-icon {
font-size: 32px;
line-height: 1;
}
.sh-component-tabs {
width: 100%;
}
/* === Components Grid === */
.sh-components-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 20px;
margin-top: 24px;
}
@media (max-width: 768px) {
.sh-components-grid {
grid-template-columns: 1fr;
}
}
/* === Component Card === */
.sh-component-card {
background: var(--sh-bg-card, #ffffff);
border-radius: 12px;
border: 1px solid var(--sh-border, #e2e8f0);
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.sh-component-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px var(--sh-hover-shadow, rgba(0, 0, 0, 0.12));
}
.sh-component-card-header {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 20px;
background: var(--sh-bg-secondary, #f8fafc);
border-bottom: 1px solid var(--sh-border, #e2e8f0);
}
.sh-component-icon {
font-size: 36px;
line-height: 1;
flex-shrink: 0;
}
.sh-component-info {
flex: 1;
min-width: 0;
}
.sh-component-name {
font-size: 18px;
font-weight: 700;
margin: 0 0 8px 0;
color: var(--sh-text-primary, #1e293b);
}
.sh-component-meta {
display: flex;
gap: 12px;
flex-wrap: wrap;
font-size: 13px;
}
.sh-component-version,
.sh-component-category {
padding: 4px 10px;
border-radius: 6px;
font-weight: 600;
}
.sh-component-version {
background: var(--sh-primary, #6366f1);
color: #ffffff;
}
.sh-component-category {
background: var(--sh-bg-tertiary, #f1f5f9);
color: var(--sh-text-secondary, #64748b);
text-transform: capitalize;
}
/* === Status Indicator === */
.sh-status-indicator {
width: 14px;
height: 14px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 4px;
}
.sh-status-running {
background: #22c55e;
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.sh-status-stopped {
background: #f59e0b;
}
.sh-status-not-installed {
background: #94a3b8;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* === Component Card Body === */
.sh-component-card-body {
padding: 16px 20px;
}
.sh-component-description {
margin: 0;
font-size: 14px;
line-height: 1.6;
color: var(--sh-text-secondary, #64748b);
}
/* === Component Actions === */
.sh-component-card-actions {
display: flex;
gap: 10px;
padding: 16px 20px;
background: var(--sh-bg-secondary, #f8fafc);
border-top: 1px solid var(--sh-border, #e2e8f0);
flex-wrap: wrap;
}
.sh-action-btn {
flex: 1;
min-width: fit-content;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.sh-btn-success {
background: #22c55e;
color: #ffffff;
}
.sh-btn-success:hover {
background: #16a34a;
transform: translateY(-2px);
}
.sh-btn-danger {
background: #ef4444;
color: #ffffff;
}
.sh-btn-danger:hover {
background: #dc2626;
transform: translateY(-2px);
}
.sh-btn-warning {
background: #f59e0b;
color: #ffffff;
}
.sh-btn-warning:hover {
background: #d97706;
transform: translateY(-2px);
}
.sh-btn-primary {
background: var(--sh-primary, #6366f1);
color: #ffffff;
}
.sh-btn-primary:hover {
background: #4f46e5;
transform: translateY(-2px);
text-decoration: none;
}
.sh-btn-secondary {
background: var(--sh-bg-tertiary, #f1f5f9);
color: var(--sh-text-secondary, #64748b);
cursor: not-allowed;
opacity: 0.6;
}
/* === Empty State === */
.sh-empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 60px 20px;
background: var(--sh-bg-secondary, #f8fafc);
border-radius: 12px;
border: 2px dashed var(--sh-border, #e2e8f0);
}
.sh-empty-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
.sh-empty-text {
font-size: 16px;
color: var(--sh-text-secondary, #64748b);
font-weight: 600;
}
/* === Dark Mode Support === */
[data-theme="dark"] {
--sh-text-primary: #f1f5f9;
--sh-text-secondary: #cbd5e1;
--sh-bg-primary: #0f172a;
--sh-bg-secondary: #1e293b;
--sh-bg-tertiary: #334155;
--sh-bg-card: #1e293b;
--sh-border: #334155;
--sh-hover-bg: #334155;
--sh-hover-shadow: rgba(0, 0, 0, 0.4);
}
[data-theme="dark"] .sh-component-card {
background: var(--sh-bg-card);
border-color: var(--sh-border);
}
[data-theme="dark"] .sh-component-card-header,
[data-theme="dark"] .sh-component-card-actions {
background: var(--sh-bg-tertiary);
border-color: var(--sh-border);
}
[data-theme="dark"] .sh-component-category {
background: var(--sh-bg-tertiary);
color: var(--sh-text-secondary);
}
[data-theme="dark"] .sh-empty-state {
background: var(--sh-bg-secondary);
border-color: var(--sh-border);
}

View File

@ -0,0 +1,836 @@
/* System Hub Dashboard - Central Control Theme * Version: 0.3.0
*/
/* Copyright (C) 2024 CyberMind.fr - Gandalf * Version: 0.3.0
*/
/* Theme-aware styles with dark/light mode support */
/* Common variables (theme-independent) */
:root {
--sh-accent-indigo: #6366f1;
--sh-accent-violet: #8b5cf6;
--sh-accent-blue: #3b82f6;
--sh-accent-cyan: #06b6d4;
--sh-accent-green: #22c55e;
--sh-accent-amber: #f59e0b;
--sh-accent-red: #ef4444;
--sh-success: #22c55e;
--sh-warning: #f59e0b;
--sh-danger: #ef4444;
--sh-info: #3b82f6;
--sh-gradient: linear-gradient(135deg, #6366f1, #8b5cf6, #a855f7);
--sh-gradient-soft: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.1));
--sh-font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--sh-font-sans: 'Inter', -apple-system, sans-serif;
--sh-radius: 8px;
--sh-radius-lg: 12px;
}
/* Dark theme (default) */
:root,
[data-theme="dark"],
[data-secubox-theme="dark"] {
--sh-bg-primary: #0a0a0f;
--sh-bg-secondary: #12121a;
--sh-bg-tertiary: #1a1a24;
--sh-border: #2a2a3a;
--sh-border-light: #3a3a4a;
--sh-text-primary: #fafafa;
--sh-text-secondary: #a0a0b0;
--sh-text-muted: #707080;
--sh-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
--sh-shadow-glow: 0 0 30px rgba(99, 102, 241, 0.3);
}
/* Light theme */
[data-theme="light"],
[data-secubox-theme="light"] {
--sh-bg-primary: #f5f5f7;
--sh-bg-secondary: #ffffff;
--sh-bg-tertiary: #f9fafb;
--sh-border: #e5e7eb;
--sh-border-light: #d1d5db;
--sh-text-primary: #0a0a0f;
--sh-text-secondary: #4b5563;
--sh-text-muted: #9ca3af;
--sh-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
--sh-shadow-glow: 0 0 30px rgba(99, 102, 241, 0.2);
}
/* Base */
.system-hub-dashboard {
font-family: var(--sh-font-sans);
background: var(--sh-bg-primary);
color: var(--sh-text-primary);
min-height: 100vh;
padding: 16px;
}
.system-hub-dashboard * { box-sizing: border-box; }
/* Header */
.sh-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0 20px;
border-bottom: 1px solid var(--sh-border);
margin-bottom: 20px;
}
.sh-logo {
display: flex;
align-items: center;
gap: 14px;
}
.sh-logo-icon {
width: 52px;
height: 52px;
background: var(--sh-gradient);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
box-shadow: var(--sh-shadow-glow);
position: relative;
}
.sh-logo-icon::after {
content: '';
position: absolute;
inset: -2px;
background: var(--sh-gradient);
border-radius: 16px;
z-index: -1;
opacity: 0.4;
filter: blur(12px);
}
.sh-logo-text {
font-size: 26px;
font-weight: 700;
}
.sh-logo-text span {
background: var(--sh-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Health Score */
.sh-health-score {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 20px;
background: var(--sh-bg-secondary);
border-radius: var(--sh-radius-lg);
border: 1px solid var(--sh-border);
}
.sh-score-circle {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--sh-font-mono);
font-size: 20px;
font-weight: 800;
position: relative;
}
.sh-score-circle::before {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
border: 4px solid var(--sh-border);
}
.sh-score-circle.healthy { background: rgba(34, 197, 94, 0.15); color: var(--sh-success); border-color: var(--sh-success); }
.sh-score-circle.warning { background: rgba(245, 158, 11, 0.15); color: var(--sh-warning); border-color: var(--sh-warning); }
.sh-score-circle.critical { background: rgba(239, 68, 68, 0.15); color: var(--sh-danger); border-color: var(--sh-danger); }
.sh-score-circle::before {
border-color: currentColor;
opacity: 0.3;
}
.sh-score-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.sh-score-label {
font-size: 14px;
font-weight: 600;
}
.sh-score-status {
font-size: 12px;
color: var(--sh-text-muted);
}
/* Stats Grid */
.sh-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 14px;
margin-bottom: 24px;
}
.sh-stat-card {
background: var(--sh-bg-secondary);
border: 1px solid var(--sh-border);
border-radius: var(--sh-radius-lg);
padding: 18px;
text-align: center;
transition: all 0.3s;
}
.sh-stat-card:hover {
transform: translateY(-3px);
box-shadow: var(--sh-shadow);
border-color: var(--sh-accent-indigo);
}
.sh-stat-icon { font-size: 26px; margin-bottom: 8px; }
.sh-stat-value {
font-size: 28px;
font-weight: 800;
font-family: var(--sh-font-mono);
background: var(--sh-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.sh-stat-label {
font-size: 11px;
color: var(--sh-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 4px;
}
/* Card */
.sh-card {
background: var(--sh-bg-secondary);
border: 1px solid var(--sh-border);
border-radius: var(--sh-radius-lg);
overflow: hidden;
margin-bottom: 20px;
}
.sh-card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--sh-border);
background: rgba(0, 0, 0, 0.3);
}
.sh-card-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 16px;
font-weight: 600;
}
.sh-card-title-icon { font-size: 22px; }
.sh-card-badge {
font-family: var(--sh-font-mono);
font-size: 12px;
font-weight: 600;
padding: 5px 12px;
border-radius: 16px;
background: var(--sh-gradient);
color: white;
}
.sh-card-body { padding: 20px; }
/* Settings + inputs */
.sh-settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
.sh-settings-grid--compact {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.sh-input-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.sh-input-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--sh-text-secondary);
}
.sh-input {
width: 100%;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--sh-border);
background: var(--sh-bg-secondary);
color: var(--sh-text-primary);
font-family: var(--sh-font-sans);
font-size: 13px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.sh-input:focus {
outline: none;
border-color: var(--sh-primary);
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.15);
}
.sh-threshold-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 16px;
}
.sh-threshold-row {
background: var(--sh-bg-secondary);
border: 1px solid var(--sh-border);
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.sh-threshold-label {
font-weight: 600;
color: var(--sh-text-primary);
}
.sh-threshold-inputs {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.sh-threshold-inputs label {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 11px;
text-transform: uppercase;
color: var(--sh-text-secondary);
flex: 1;
min-width: 120px;
}
.sh-support-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
}
.sh-support-card {
background: var(--sh-bg-secondary);
border: 1px solid var(--sh-border);
border-radius: 12px;
padding: 14px;
display: flex;
flex-direction: column;
gap: 6px;
}
.sh-support-label {
font-size: 11px;
text-transform: uppercase;
color: var(--sh-text-secondary);
letter-spacing: 0.06em;
}
/* Component Grid */
.sh-components-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.sh-component-card {
background: var(--sh-bg-tertiary);
border: 1px solid var(--sh-border);
border-radius: var(--sh-radius-lg);
padding: 20px;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
.sh-component-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--component-color);
}
.sh-component-card:hover {
transform: translateY(-3px);
box-shadow: var(--sh-shadow);
}
.sh-component-card.planned {
opacity: 0.6;
border-style: dashed;
}
.sh-component-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 12px;
}
.sh-component-info {
display: flex;
align-items: center;
gap: 12px;
}
.sh-component-icon {
width: 44px;
height: 44px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--component-color);
color: var(--component-color);
}
.sh-component-name {
font-size: 15px;
font-weight: 700;
}
.sh-component-desc {
font-size: 11px;
color: var(--sh-text-muted);
}
.sh-component-status {
padding: 4px 10px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
}
.sh-component-status.running {
background: rgba(34, 197, 94, 0.15);
color: var(--sh-success);
}
.sh-component-status.stopped {
background: rgba(239, 68, 68, 0.15);
color: var(--sh-danger);
}
.sh-component-status.planned {
background: rgba(99, 102, 241, 0.15);
color: var(--sh-accent-indigo);
}
.sh-component-actions {
display: flex;
gap: 8px;
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid var(--sh-border);
}
.sh-component-action {
flex: 1;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid var(--sh-border);
background: var(--sh-bg-secondary);
color: var(--sh-text-secondary);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.sh-component-action:hover {
border-color: var(--sh-accent-indigo);
color: var(--sh-text-primary);
}
/* Health Metrics */
.sh-health-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.sh-health-metric {
background: var(--sh-bg-tertiary);
border: 1px solid var(--sh-border);
border-radius: var(--sh-radius);
padding: 16px;
}
.sh-metric-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.sh-metric-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
}
.sh-metric-icon { font-size: 18px; }
.sh-metric-value {
font-family: var(--sh-font-mono);
font-size: 14px;
font-weight: 600;
}
.sh-metric-value.ok { color: var(--sh-success); }
.sh-metric-value.warning { color: var(--sh-warning); }
.sh-metric-value.critical { color: var(--sh-danger); }
.sh-progress-bar {
height: 8px;
background: var(--sh-bg-primary);
border-radius: 4px;
overflow: hidden;
}
.sh-progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.5s;
}
.sh-progress-fill.ok { background: var(--sh-success); }
.sh-progress-fill.warning { background: var(--sh-warning); }
.sh-progress-fill.critical { background: var(--sh-danger); }
/* Remote Section */
.sh-remote-card {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(139, 92, 246, 0.05));
border-color: rgba(99, 102, 241, 0.3);
}
.sh-remote-id {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: var(--sh-bg-tertiary);
border-radius: var(--sh-radius);
margin-bottom: 16px;
}
.sh-remote-id-icon {
width: 60px;
height: 60px;
background: var(--sh-gradient);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
}
.sh-remote-id-value {
font-family: var(--sh-font-mono);
font-size: 28px;
font-weight: 800;
letter-spacing: 2px;
}
.sh-remote-id-label {
font-size: 12px;
color: var(--sh-text-muted);
}
/* Logs */
.sh-log-list {
max-height: 400px;
overflow-y: auto;
}
.sh-log-item {
display: flex;
gap: 12px;
padding: 10px;
border-bottom: 1px solid var(--sh-border);
font-size: 12px;
}
.sh-log-item:last-child { border-bottom: none; }
.sh-log-time {
font-family: var(--sh-font-mono);
font-size: 10px;
color: var(--sh-text-muted);
min-width: 140px;
}
.sh-log-source {
padding: 2px 8px;
border-radius: 4px;
font-size: 9px;
font-weight: 600;
background: var(--sh-bg-tertiary);
color: var(--sh-accent-indigo);
min-width: 80px;
text-align: center;
}
.sh-log-level {
padding: 2px 8px;
border-radius: 4px;
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
min-width: 55px;
text-align: center;
}
.sh-log-level.info { background: rgba(59, 130, 246, 0.15); color: var(--sh-info); }
.sh-log-level.warning { background: rgba(245, 158, 11, 0.15); color: var(--sh-warning); }
.sh-log-level.error { background: rgba(239, 68, 68, 0.15); color: var(--sh-danger); }
.sh-log-message { flex: 1; color: var(--sh-text-secondary); }
/* Toggle */
.sh-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
background: var(--sh-bg-tertiary);
border-radius: var(--sh-radius);
margin-bottom: 10px;
}
.sh-toggle-info { display: flex; align-items: center; gap: 12px; }
.sh-toggle-icon { font-size: 20px; }
.sh-toggle-label { font-size: 14px; font-weight: 500; }
.sh-toggle-desc { font-size: 11px; color: var(--sh-text-muted); }
.sh-toggle-switch {
width: 48px;
height: 26px;
background: var(--sh-bg-primary);
border-radius: 13px;
position: relative;
cursor: pointer;
transition: background 0.3s;
}
.sh-toggle-switch.active { background: var(--sh-accent-indigo); }
.sh-toggle-switch::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
top: 3px;
left: 3px;
transition: transform 0.3s;
}
.sh-toggle-switch.active::after { transform: translateX(22px); }
/* Form */
.sh-form-group { margin-bottom: 16px; }
.sh-form-label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 8px; color: var(--sh-text-secondary); }
.sh-form-hint { font-size: 11px; color: var(--sh-text-muted); margin-top: 6px; }
.sh-input,
.sh-select {
width: 100%;
padding: 12px 16px;
background: var(--sh-bg-primary);
border: 1px solid var(--sh-border);
border-radius: var(--sh-radius);
color: var(--sh-text-primary);
font-size: 14px;
font-family: var(--sh-font-mono);
transition: border-color 0.2s;
}
.sh-input:focus,
.sh-select:focus {
outline: none;
border-color: var(--sh-accent-indigo);
}
/* Buttons */
.sh-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
border: 1px solid var(--sh-border);
border-radius: var(--sh-radius);
background: var(--sh-bg-tertiary);
color: var(--sh-text-primary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.sh-btn:hover { border-color: var(--sh-accent-indigo); }
.sh-btn-primary {
background: var(--sh-gradient);
border: none;
color: white;
}
.sh-btn-primary:hover {
box-shadow: var(--sh-shadow-glow);
transform: translateY(-2px);
}
.sh-btn-success { background: var(--sh-success); border: none; color: white; }
.sh-btn-danger { background: var(--sh-danger); border: none; color: white; }
.sh-btn-group { display: flex; gap: 12px; flex-wrap: wrap; }
/* Roadmap */
.sh-roadmap-item {
display: flex;
align-items: center;
gap: 16px;
padding: 14px;
background: var(--sh-bg-tertiary);
border: 1px dashed var(--sh-border);
border-radius: var(--sh-radius);
margin-bottom: 10px;
}
.sh-roadmap-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
background: rgba(99, 102, 241, 0.1);
border: 1px solid var(--sh-accent-indigo);
color: var(--sh-accent-indigo);
}
.sh-roadmap-info { flex: 1; }
.sh-roadmap-name { font-size: 14px; font-weight: 600; }
.sh-roadmap-desc { font-size: 11px; color: var(--sh-text-muted); }
.sh-roadmap-date {
padding: 6px 12px;
background: var(--sh-bg-primary);
border-radius: 8px;
font-family: var(--sh-font-mono);
font-size: 12px;
color: var(--sh-accent-indigo);
}
/* System Info */
.sh-sysinfo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.sh-sysinfo-item {
display: flex;
justify-content: space-between;
padding: 10px 14px;
background: var(--sh-bg-tertiary);
border-radius: 6px;
}
.sh-sysinfo-label {
font-size: 12px;
color: var(--sh-text-muted);
}
.sh-sysinfo-value {
font-family: var(--sh-font-mono);
font-size: 12px;
font-weight: 600;
}
/* Responsive */
@media (max-width: 768px) {
.sh-header { flex-direction: column; gap: 16px; align-items: flex-start; }
.sh-stats-grid { grid-template-columns: repeat(2, 1fr); }
.sh-components-grid { grid-template-columns: 1fr; }
.sh-health-grid { grid-template-columns: 1fr; }
.sh-btn-group { flex-direction: column; }
.sh-btn { width: 100%; justify-content: center; }
}
/* Scrollbar */
.system-hub-dashboard ::-webkit-scrollbar { width: 8px; height: 8px; }
.system-hub-dashboard ::-webkit-scrollbar-track { background: var(--sh-bg-tertiary); }
.system-hub-dashboard ::-webkit-scrollbar-thumb { background: var(--sh-border); border-radius: 4px; }
.system-hub-dashboard ::-webkit-scrollbar-thumb:hover { background: var(--sh-text-muted); }
/* Animations */
@keyframes pulse-sh {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.sh-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse-sh 1.5s ease-in-out infinite;
}
.sh-status-dot.running { background: var(--sh-success); }
.sh-status-dot.stopped { background: var(--sh-danger); }
.sh-status-dot.warning { background: var(--sh-warning); }

View File

@ -0,0 +1,68 @@
.sh-health-view { padding: 28px; }
.sh-health-hero {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px;
background: var(--sh-gradient-soft);
border-radius: 20px;
border: 1px solid var(--sh-border);
box-shadow: var(--sh-shadow);
margin-bottom: 24px;
}
.sh-health-score {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 140px;
height: 140px;
border-radius: 50%;
border: 8px solid rgba(255,255,255,0.08);
font-size: 32px;
font-weight: 700;
}
.sh-health-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.sh-health-card {
background: var(--sh-bg-secondary);
border-radius: 16px;
border: 1px solid var(--sh-border);
padding: 16px;
box-shadow: var(--sh-shadow);
}
.sh-health-value { font-size: 28px; font-weight: 700; }
.sh-health-bar {
margin-top: 12px;
height: 6px;
background: rgba(255,255,255,0.08);
border-radius: 999px;
overflow: hidden;
}
.sh-health-bar-fill {
height: 100%;
background: linear-gradient(90deg,#22c55e,#3b82f6);
transition: width 0.3s ease;
}
.sh-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.sh-summary-card {
background: var(--sh-bg-secondary);
border: 1px solid var(--sh-border);
border-radius: 16px;
padding: 18px;
box-shadow: var(--sh-shadow);
}
.sh-reco-list { list-style: none; padding: 0; margin: 0; }
.sh-reco-list li { padding: 10px; border-bottom: 1px solid var(--sh-border); }
@media (max-width: 720px) {
.sh-health-hero { flex-direction: column; gap: 16px; }
}

View File

@ -0,0 +1,247 @@
.sh-logs-view {
padding: 28px;
background: radial-gradient(circle at top, rgba(248,113,113,0.08), transparent),
radial-gradient(circle at bottom, rgba(14,165,233,0.08), transparent),
var(--sh-bg);
border-radius: 22px;
}
.sh-logs-hero {
background: linear-gradient(135deg, rgba(248,113,113,0.16), rgba(248,159,99,0.16));
border-radius: 22px;
padding: 24px;
border: 1px solid var(--sh-border);
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 18px;
box-shadow: 0 25px 40px rgba(0,0,0,0.35);
}
.sh-logs-hero h1 {
margin: 0;
font-size: 26px;
}
.sh-logs-hero p {
margin: 6px 0 0;
color: var(--sh-text-secondary);
}
.sh-log-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
min-width: 280px;
}
.sh-log-stat {
background: rgba(15,23,42,0.45);
border-radius: 16px;
padding: 12px;
border: 1px solid rgba(255,255,255,0.08);
text-align: center;
}
.sh-log-stat .label {
display: block;
font-size: 12px;
text-transform: uppercase;
color: var(--sh-text-secondary);
letter-spacing: 0.12em;
}
.sh-log-stat .value {
font-size: 28px;
font-weight: 700;
line-height: 1;
font-family: 'JetBrains Mono', monospace;
}
.sh-log-stat.danger .value {
color: #f87171;
}
.sh-log-stat.warn .value {
color: #fbbf24;
}
.sh-log-controls {
margin: 26px 0 20px;
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
}
.sh-log-search {
flex: 1;
min-width: 260px;
}
.sh-log-search input {
width: 100%;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.12);
background: rgba(255,255,255,0.02);
padding: 12px 16px;
color: var(--sh-text-primary);
font-size: 15px;
}
.sh-log-selectors {
display: flex;
gap: 12px;
align-items: center;
}
.sh-log-selectors select {
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.12);
background: rgba(15,23,42,0.8);
color: var(--sh-text-primary);
padding: 10px 14px;
}
.sh-toggle {
display: flex;
gap: 6px;
align-items: center;
font-size: 13px;
color: var(--sh-text-secondary);
}
.sh-toggle input {
width: 16px;
height: 16px;
}
.sh-btn {
border-radius: 12px;
padding: 10px 18px;
border: 1px solid rgba(255,255,255,0.12);
background: rgba(99,102,241,0.18);
color: white;
font-weight: 600;
cursor: pointer;
}
.sh-btn-primary {
background: linear-gradient(135deg,#6366f1,#a855f7);
border-color: transparent;
}
.sh-logs-body {
display: grid;
grid-template-columns: 3fr 1fr;
gap: 20px;
}
.sh-log-panel {
background: var(--sh-bg-card);
border-radius: 20px;
padding: 16px;
border: 1px solid var(--sh-border);
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
gap: 12px;
}
.sh-log-filters {
margin-bottom: 8px;
}
.sh-log-stream {
background: rgba(0,0,0,0.35);
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.08);
padding: 14px;
height: 520px;
overflow-y: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
color: var(--sh-text-primary);
}
.sh-log-line {
display: flex;
gap: 14px;
padding: 6px 4px;
border-bottom: 1px solid rgba(255,255,255,0.03);
}
.sh-log-line.error {
background: rgba(248,113,113,0.08);
}
.sh-log-line.warning {
background: rgba(251,191,36,0.08);
}
.sh-log-index {
color: var(--sh-text-muted);
min-width: 40px;
text-align: right;
}
.sh-log-message {
white-space: pre-wrap;
}
.sh-log-side {
background: rgba(15,23,42,0.85);
border-radius: 20px;
padding: 18px;
border: 1px solid rgba(255,255,255,0.08);
box-shadow: 0 20px 40px rgba(0,0,0,0.25);
}
.sh-log-side h3 {
margin: 0 0 12px;
font-size: 18px;
}
.sh-log-side ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.sh-log-side li {
display: flex;
justify-content: space-between;
font-size: 14px;
border-bottom: 1px dashed rgba(255,255,255,0.1);
padding-bottom: 6px;
}
.sh-log-side strong {
font-family: 'JetBrains Mono', monospace;
}
@media (max-width: 1024px) {
.sh-logs-body {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.sh-logs-view {
padding: 16px;
}
.sh-log-controls {
flex-direction: column;
align-items: stretch;
}
.sh-log-selectors {
flex-direction: column;
align-items: stretch;
}
}

View File

@ -0,0 +1,268 @@
.sh-overview {
padding: 28px;
background: radial-gradient(circle at top, rgba(99,102,241,0.15), transparent),
radial-gradient(circle at bottom, rgba(14,165,233,0.12), transparent),
var(--sh-bg);
border-radius: 22px;
}
.sh-hero {
background: linear-gradient(135deg, rgba(99,102,241,0.18), rgba(14,165,233,0.18));
border-radius: 20px;
padding: 28px;
border: 1px solid var(--sh-border);
display: grid;
grid-template-columns: 2fr 2fr 1fr;
gap: 20px;
align-items: center;
box-shadow: 0 20px 40px rgba(0,0,0,0.35);
}
.sh-hero-title {
display: flex;
align-items: center;
gap: 18px;
}
.sh-hero-icon {
width: 64px;
height: 64px;
border-radius: 18px;
background: rgba(15,23,42,0.4);
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
}
.sh-hero-title h1 {
margin: 0;
font-size: 28px;
color: var(--sh-text-primary);
}
.sh-hero-title p {
margin: 4px 0 0;
color: var(--sh-text-secondary);
}
.sh-hero-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.sh-badge {
padding: 8px 14px;
border-radius: 999px;
background: rgba(15,23,42,0.6);
border: 1px solid rgba(255,255,255,0.1);
font-weight: 600;
cursor: default;
}
.sh-badge.ghost {
background: rgba(255,255,255,0.1);
}
.sh-badge-copy {
cursor: pointer;
}
.sh-hero-score {
text-align: center;
background: rgba(15,23,42,0.6);
padding: 16px;
border-radius: 18px;
border: 1px solid rgba(255,255,255,0.08);
}
.sh-score-value {
font-size: 42px;
font-weight: 700;
}
.sh-score-label {
text-transform: uppercase;
font-size: 12px;
letter-spacing: 0.18em;
color: var(--sh-text-secondary);
}
.sh-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
margin: 26px 0;
}
.sh-info-card {
background: var(--sh-bg-card);
border-radius: 18px;
border: 1px solid var(--sh-border);
padding: 18px;
box-shadow: 0 10px 25px rgba(0,0,0,0.25);
position: relative;
}
.sh-info-label {
font-size: 12px;
text-transform: uppercase;
color: var(--sh-text-secondary);
letter-spacing: 0.15em;
margin-bottom: 10px;
}
.sh-info-value {
font-size: 22px;
font-weight: 600;
color: var(--sh-text-primary);
}
.sh-info-value.mono {
font-family: 'JetBrains Mono', monospace;
}
.sh-info-action {
position: absolute;
top: 14px;
right: 14px;
border: none;
background: rgba(99,102,241,0.18);
color: var(--sh-text-primary);
font-weight: 600;
padding: 6px 12px;
border-radius: 999px;
cursor: pointer;
}
.sh-monitor-panel,
.sh-status-panel {
background: var(--sh-bg-card);
border-radius: 20px;
border: 1px solid var(--sh-border);
box-shadow: 0 15px 35px rgba(0,0,0,0.25);
padding: 22px;
margin-bottom: 26px;
}
.sh-section-header {
margin-bottom: 16px;
}
.sh-section-header h2 {
margin: 0;
font-size: 20px;
}
.sh-section-header p {
margin: 4px 0 0;
color: var(--sh-text-secondary);
}
.sh-monitor-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 18px;
}
.sh-monitor-card {
background: rgba(255,255,255,0.02);
border-radius: 18px;
border: 1px solid rgba(255,255,255,0.05);
padding: 18px;
display: grid;
grid-template-columns: auto 1fr auto;
gap: 12px;
align-items: center;
}
.sh-monitor-icon {
font-size: 28px;
}
.sh-monitor-info {
display: flex;
flex-direction: column;
}
.sh-monitor-label {
font-weight: 600;
}
.sh-monitor-detail {
color: var(--sh-text-secondary);
font-size: 14px;
}
.sh-monitor-progress {
grid-column: 2 / span 2;
height: 7px;
background: rgba(255,255,255,0.08);
border-radius: 999px;
overflow: hidden;
}
.sh-monitor-bar {
height: 100%;
background: linear-gradient(90deg, #22d3ee, #a855f7);
transition: width 0.25s ease;
}
.sh-monitor-percent {
font-family: 'JetBrains Mono', monospace;
}
.sh-status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.sh-status-card {
border-radius: 18px;
padding: 16px;
border: 1px solid rgba(255,255,255,0.08);
display: flex;
gap: 14px;
background: rgba(255,255,255,0.025);
}
.sh-status-icon {
font-size: 20px;
}
.sh-status-card.ok {
border-left: 3px solid #22c55e;
}
.sh-status-card.warn {
border-left: 3px solid #f97316;
}
.sh-status-card.unknown {
border-left: 3px solid #9ca3af;
}
.sh-status-value {
font-weight: 600;
}
.sh-status-extra {
display: block;
font-size: 13px;
color: var(--sh-text-secondary);
margin-top: 4px;
}
@media (max-width: 960px) {
.sh-hero {
grid-template-columns: 1fr;
}
}
@media (max-width: 600px) {
.sh-overview {
padding: 16px;
}
}

View File

@ -0,0 +1,219 @@
.sh-services-view {
padding: 28px;
background: radial-gradient(circle at top, rgba(15,118,255,0.12), transparent),
radial-gradient(circle at bottom, rgba(99,102,241,0.1), transparent),
var(--sh-bg);
border-radius: 22px;
}
.sh-services-hero {
background: linear-gradient(135deg, rgba(99,102,241,0.18), rgba(6,182,212,0.18));
border-radius: 22px;
padding: 24px;
border: 1px solid var(--sh-border);
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 16px;
box-shadow: 0 20px 40px rgba(0,0,0,0.35);
}
.sh-services-hero h1 {
margin: 0;
font-size: 26px;
}
.sh-services-hero p {
margin: 4px 0 0;
color: var(--sh-text-secondary);
}
.sh-services-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
min-width: 300px;
}
.sh-service-stat {
background: rgba(15,23,42,0.45);
border-radius: 16px;
padding: 12px;
border: 1px solid rgba(255,255,255,0.08);
text-align: center;
}
.sh-service-stat .label {
display: block;
font-size: 12px;
text-transform: uppercase;
color: var(--sh-text-secondary);
letter-spacing: 0.15em;
}
.sh-service-stat .value {
font-size: 28px;
font-weight: 700;
line-height: 1;
}
.sh-service-stat.success .value {
color: #22c55e;
}
.sh-service-stat.danger .value {
color: #ef4444;
}
.sh-service-controls {
margin: 24px 0;
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
justify-content: space-between;
}
.sh-service-tabs {
flex: 1;
min-width: 220px;
}
.sh-service-search input {
width: 260px;
max-width: 100%;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.12);
background: rgba(255,255,255,0.02);
padding: 10px 16px;
color: var(--sh-text-primary);
}
.sh-services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 18px;
}
.sh-service-card {
background: var(--sh-bg-card);
border-radius: 18px;
border: 1px solid rgba(255,255,255,0.08);
padding: 18px;
box-shadow: 0 12px 30px rgba(0,0,0,0.25);
display: flex;
flex-direction: column;
gap: 16px;
}
.sh-service-card.running {
border-left: 4px solid #22c55e;
}
.sh-service-card.stopped {
border-left: 4px solid #ef4444;
}
.sh-service-head {
display: flex;
justify-content: space-between;
gap: 10px;
}
.sh-service-head h3 {
margin: 0;
font-size: 20px;
}
.sh-service-tag {
font-size: 12px;
color: var(--sh-text-secondary);
}
.sh-service-status {
padding: 6px 12px;
border-radius: 999px;
font-weight: 600;
}
.sh-service-status.running {
background: rgba(34,197,94,0.18);
color: #22c55e;
}
.sh-service-status.stopped {
background: rgba(239,68,68,0.18);
color: #ef4444;
}
.sh-service-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.sh-btn {
border-radius: 12px;
padding: 10px;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.03);
color: var(--sh-text-primary);
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.sh-btn:hover {
background: rgba(255,255,255,0.09);
}
.sh-btn-ghost {
grid-column: span 2;
background: rgba(99,102,241,0.15);
border-color: rgba(99,102,241,0.35);
}
.sh-empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: var(--sh-text-secondary);
border: 1px dashed rgba(255,255,255,0.15);
border-radius: 18px;
}
.sh-empty-icon {
font-size: 40px;
margin-bottom: 8px;
}
.sh-service-detail {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 18px;
}
.sh-service-detail-row {
display: flex;
justify-content: space-between;
border-bottom: 1px solid rgba(148,163,184,0.2);
padding-bottom: 6px;
font-size: 14px;
}
@media (max-width: 720px) {
.sh-services-view {
padding: 16px;
}
.sh-service-controls {
flex-direction: column;
align-items: stretch;
}
.sh-service-search input {
width: 100%;
}
}

View File

@ -0,0 +1,37 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
include $(INCLUDE_DIR)/package.mk
define Package/secubox-app
SECTION:=utils
CATEGORY:=Utilities
TITLE:=SecuBox App Store CLI
DEPENDS:=+jsonfilter
endef
define Package/secubox-app/description
Command line helper for SecuBox App Store manifests. Installs /usr/sbin/secubox-app
and ships the default manifests under /usr/share/secubox/plugins/.
endef
define Build/Prepare
$(CP) ./files $(PKG_BUILD_DIR)/
endef
define Build/Configure
endef
define Build/Compile
endef
define Package/secubox-app/install
$(CP) $(PKG_BUILD_DIR)/files/* $(1)/
endef
$(eval $(call BuildPackage,secubox-app))

View File

@ -0,0 +1,253 @@
#!/bin/sh
# SecuBox Apps CLI
# Lists/installs/removes plugin manifests declared under plugins/*/manifest.json
set -eu
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
DEFAULT_PLUGINS_DIR="/usr/share/secubox/plugins"
if [ -n "${SECUBOX_PLUGINS_DIR:-}" ]; then
PLUGINS_DIR="$SECUBOX_PLUGINS_DIR"
elif [ -d "$SCRIPT_DIR/../plugins" ]; then
PLUGINS_DIR=$(cd "$SCRIPT_DIR/../plugins" && pwd)
else
PLUGINS_DIR="$DEFAULT_PLUGINS_DIR"
fi
PKG_MGR=""
PKG_UPDATED=0
info() { printf '[INFO] %s\n' "$*"; }
warn() { printf '[WARN] %s\n' "$*" >&2; }
err() { printf '[ERROR] %s\n' "$*" >&2; }
usage() {
cat <<'USAGE'
SecuBox Apps CLI
Usage: secubox-app <command> [arguments]
Commands:
list Show available app manifests
show <app-id> Display manifest details
install <app-id> Install required packages + run install action
remove <app-id> Remove packages listed in manifest
status <app-id> Show install state and run status action if defined
update <app-id> Run plugin update action or opkg upgrade
Environment:
SECUBOX_PLUGINS_DIR Override manifest directory (default: /usr/share/secubox/plugins)
USAGE
}
require_tool() {
command -v "$1" >/dev/null 2>&1 || { err "Missing dependency: $1"; exit 1; }
}
require_jsonfilter() { require_tool jsonfilter; }
ensure_pkg_mgr() {
if [ -n "$PKG_MGR" ]; then
return
fi
if command -v opkg >/dev/null 2>&1; then
PKG_MGR="opkg"
elif command -v apk >/dev/null 2>&1; then
PKG_MGR="apk"
else
err "Missing dependency: require opkg or apk"
exit 1
fi
}
pkg_update_once() {
ensure_pkg_mgr
[ "$PKG_UPDATED" -eq 1 ] && return
case "$PKG_MGR" in
opkg) opkg update >/dev/null ;;
apk) apk update >/dev/null ;;
esac
PKG_UPDATED=1
}
pkg_is_installed() {
ensure_pkg_mgr
case "$PKG_MGR" in
opkg) opkg status "$1" >/dev/null 2>&1 ;;
apk) apk info -e "$1" >/dev/null 2>&1 ;;
esac
}
pkg_install() {
local pkg="$1"
if pkg_is_installed "$pkg"; then
info "$pkg already installed"
return
fi
info "Installing $pkg"
pkg_update_once
case "$PKG_MGR" in
opkg) opkg install "$pkg" ;;
apk) apk add "$pkg" ;;
esac
}
pkg_remove() {
local pkg="$1"
if ! pkg_is_installed "$pkg"; then
info "$pkg not installed"
return
fi
info "Removing $pkg"
case "$PKG_MGR" in
opkg) opkg remove "$pkg" ;;
apk) apk del "$pkg" ;;
esac
}
pkg_upgrade() {
case "$PKG_MGR" in
opkg) opkg upgrade "$1" ;;
apk) apk upgrade "$1" ;;
esac
}
manifest_path() {
local id="$1"
local file="$PLUGINS_DIR/$id/manifest.json"
[ -f "$file" ] || { err "Manifest not found for '$id' ($file)"; exit 1; }
printf '%s' "$file"
}
manifest_field() {
local file="$1"; shift
jsonfilter -f "$file" -e "$1" 2>/dev/null || true
}
manifest_packages() {
local file="$1"
jsonfilter -f "$file" -e '@.packages[*]' 2>/dev/null || true
}
manifest_action() {
local file="$1"; shift
jsonfilter -f "$file" -e "@.actions.$1" 2>/dev/null || true
}
plugin_state() {
local file="$1"
local pkgs pkg missing=0 installed=0 total=0
pkgs=$(manifest_packages "$file")
for pkg in $pkgs; do
total=$((total+1))
if pkg_is_installed "$pkg"; then
installed=$((installed+1))
else
missing=$((missing+1))
fi
done
if [ "$total" -eq 0 ]; then
echo "n/a"
elif [ "$missing" -eq 0 ]; then
echo "installed"
elif [ "$installed" -eq 0 ]; then
echo "missing"
else
echo "partial"
fi
}
list_plugins() {
require_jsonfilter
ensure_pkg_mgr
printf '%-16s %-22s %-10s %-10s\n' "ID" "Name" "Type" "State"
printf '%-16s %-22s %-10s %-10s\n' "--" "----" "----" "-----"
find "$PLUGINS_DIR" -mindepth 2 -maxdepth 2 -name manifest.json | sort | while read -r file; do
local id name type state
id=$(manifest_field "$file" '@.id')
name=$(manifest_field "$file" '@.name')
type=$(manifest_field "$file" '@.type')
state=$(plugin_state "$file")
printf '%-16s %-22s %-10s %-10s\n' "$id" "${name:-Unknown}" "${type:-?}" "$state"
done
}
show_manifest() {
require_jsonfilter
local file=$(manifest_path "$1")
cat "$file"
}
install_plugin() {
require_jsonfilter
ensure_pkg_mgr
local file=$(manifest_path "$1")
local pkgs pkg
pkgs=$(manifest_packages "$file")
if [ -z "$pkgs" ]; then
warn "Manifest has no packages; nothing to install."
else
for pkg in $pkgs; do
pkg_install "$pkg"
done
fi
local install_cmd
install_cmd=$(manifest_action "$file" install)
if [ -n "$install_cmd" ]; then
info "Running install action: $install_cmd"
sh -c "$install_cmd"
fi
}
remove_plugin() {
require_jsonfilter
ensure_pkg_mgr
local file=$(manifest_path "$1")
local pkgs pkg
pkgs=$(manifest_packages "$file")
for pkg in $pkgs; do
pkg_remove "$pkg"
done
}
plugin_status_cmd() {
require_jsonfilter
local file=$(manifest_path "$1")
local status_cmd
status_cmd=$(manifest_action "$file" status)
local state
state=$(plugin_state "$file")
printf 'App: %s\nState: %s\n' "$1" "$state"
if [ -n "$status_cmd" ]; then
printf 'Status command output:\n'
sh -c "$status_cmd" || true
fi
}
update_plugin() {
require_jsonfilter
ensure_pkg_mgr
local file=$(manifest_path "$1")
local update_cmd pkgs pkg
update_cmd=$(manifest_action "$file" update)
if [ -n "$update_cmd" ]; then
info "Running update action: $update_cmd"
sh -c "$update_cmd"
else
pkgs=$(manifest_packages "$file")
for pkg in $pkgs; do
info "Upgrading $pkg"
pkg_update_once
pkg_upgrade "$pkg" || true
done
fi
}
case "${1:-}" in
list) shift; list_plugins ;;
show) shift; [ $# -ge 1 ] || { err "show requires an app id"; exit 1; }; show_manifest "$1" ;;
install) shift; [ $# -ge 1 ] || { err "install requires an app id"; exit 1; }; install_plugin "$1" ;;
remove) shift; [ $# -ge 1 ] || { err "remove requires an app id"; exit 1; }; remove_plugin "$1" ;;
status) shift; [ $# -ge 1 ] || { err "status requires an app id"; exit 1; }; plugin_status_cmd "$1" ;;
update) shift; [ $# -ge 1 ] || { err "update requires an app id"; exit 1; }; update_plugin "$1" ;;
help|--help|-h|'') usage ;;
*) err "Unknown command: $1"; usage; exit 1 ;;
esac

1
plugins Symbolic link
View File

@ -0,0 +1 @@
package/secubox/secubox-app/files/usr/share/secubox/plugins

1
profiles Symbolic link
View File

@ -0,0 +1 @@
../luci-app-secubox/profiles

1
secubox-app-domoticz Symbolic link
View File

@ -0,0 +1 @@
package/secubox/secubox-app-domoticz

1
secubox-app-lyrion Symbolic link
View File

@ -0,0 +1 @@
package/secubox/secubox-app-lyrion

1
secubox-app-zigbee2mqtt Symbolic link
View File

@ -0,0 +1 @@
package/secubox/secubox-app-zigbee2mqtt

View File

@ -549,6 +549,7 @@ copy_packages() {
# Use the local feed directory (outside SDK)
local feed_dir="../local-feed"
mkdir -p "$feed_dir"
local -a core_pkg_names=()
if [[ -n "$single_package" ]]; then
print_info "Copying single package: $single_package"
@ -594,14 +595,24 @@ copy_packages() {
fi
done
# Copy core packages (non-LuCI)
for pkg in ../../package/secubox/nodogsplash/; do
# Copy secubox-app-* helper packages
for pkg in ../../package/secubox/secubox-app-*/; do
if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then
local pkg_name=$(basename "$pkg")
echo " 📁 $pkg_name"
cp -r "$pkg" "$feed_dir/"
fi
done
# Copy core packages (non-LuCI)
for pkg in ../../package/secubox/*/; do
if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then
local pkg_name=$(basename "$pkg")
echo " 📁 $pkg_name"
cp -r "$pkg" "$feed_dir/"
core_pkg_names+=("$pkg_name")
fi
done
fi
echo ""
@ -639,10 +650,10 @@ copy_packages() {
fi
done
# Install core packages (non-LuCI)
for pkg in "$feed_dir"/nodogsplash/; do
if [[ -d "$pkg" ]]; then
local pkg_name=$(basename "$pkg")
# Install secubox core packages
for pkg_name in "${core_pkg_names[@]}"; do
local pkg_path="$feed_dir/$pkg_name"
if [[ -d "$pkg_path" ]]; then
echo " Installing $pkg_name..."
./scripts/feeds install "$pkg_name" 2>&1 | grep -v "WARNING:" || true
fi
@ -764,6 +775,14 @@ build_packages() {
for pkg in feeds/secubox/luci-theme-*/; do
[[ -d "$pkg" ]] && packages_to_build+=("$(basename "$pkg")")
done
# Build core secubox packages (secubox-app, nodogsplash, etc.)
for pkg in feeds/secubox/secubox-*/; do
[[ -d "$pkg" ]] && packages_to_build+=("$(basename "$pkg")")
done
for pkg in feeds/secubox/nodogsplash/; do
[[ -d "$pkg" ]] && packages_to_build+=("$(basename "$pkg")")
done
fi
# Build packages
@ -1048,8 +1067,18 @@ copy_secubox_to_openwrt() {
fi
done
# Copy additional core packages (non-LuCI)
for pkg in ../../package/secubox/nodogsplash/; do
# Copy secubox-app-* helper packages
for pkg in ../../package/secubox/secubox-app-*/; do
if [[ -d "$pkg" ]]; then
local pkg_name=$(basename "$pkg")
echo "$pkg_name"
cp -r "$pkg" package/secubox/
pkg_count=$((pkg_count + 1))
fi
done
# Copy additional core packages (non-LuCI / non-app store)
for pkg in ../../package/secubox/*/; do
if [[ -d "$pkg" ]]; then
local pkg_name=$(basename "$pkg")
echo "$pkg_name"

View File

@ -23,6 +23,12 @@ SRC_PATH=""
GIT_URL=""
GIT_BRANCH=""
POST_CMD=""
BACKUP_DIR="${BACKUP_DIR:-/tmp/quickdeploy-backups}"
HISTORY_DIR="${HOME}/.secubox"
HISTORY_FILE="$HISTORY_DIR/quickdeploy-history.log"
LAST_BACKUP_FILE="$HISTORY_DIR/quickdeploy-last"
LAST_BACKUP=""
UNINSTALL_TARGET=""
usage() {
cat <<'USAGE'
@ -34,6 +40,7 @@ Options (choose one source):
--ipk <file.ipk> Upload + install an IPK via opkg.
--apk <file.apk> Upload + install an APK via apk add.
--src <path> Tar + upload a local directory to --target-path.
--src-clean <path> Remove files previously deployed from a local directory (no upload).
--git <repo_url> Clone repo (optionally --branch) then upload.
--profile <name> Use a predefined deployment profile (e.g. theme, luci-app).
--app <name> Shortcut for --profile luci-app; auto-resolves `luci-app-<name>`
@ -48,6 +55,7 @@ Common flags:
--no-verify Skip post-deploy file verification.
--force-root Allow --src to write directly under /. Use with caution.
--no-auto-profile Disable automatic LuCI app detection when using --src.
--uninstall [backup] Restore the latest (or specific) quick-deploy backup.
--post <command> Extra remote command to run after deploy.
-h, --help Show this message.
@ -79,6 +87,87 @@ join_path() {
fi
}
gather_deploy_files() {
local dir="$1"
local -n out_ref="$2"
out_ref=()
[[ ! -d "$dir" ]] && return 0
if [[ ${#INCLUDE_PATHS[@]} -gt 0 ]]; then
for inc in "${INCLUDE_PATHS[@]}"; do
local path="$dir/$inc"
if [[ -d "$path" ]]; then
while IFS= read -r f; do out_ref+=("$f"); done < <(find "$path" -type f -not -path '*/.git/*' | sort)
elif [[ -f "$path" ]]; then
out_ref+=("$path")
fi
done
else
while IFS= read -r f; do out_ref+=("$f"); done < <(find "$dir" -type f -not -path '*/.git/*' | sort)
fi
}
record_backup_metadata() {
local backup_file="$1"
[[ -z "$backup_file" ]] && return
mkdir -p "$HISTORY_DIR"
printf '%s | %s | %s\n' "$(date -Iseconds)" "$ROUTER" "$backup_file" >> "$HISTORY_FILE"
printf '%s\n' "$backup_file" > "$LAST_BACKUP_FILE"
LAST_BACKUP="$backup_file"
log "🗂 Backup saved to $backup_file (restore with --uninstall $(basename "$backup_file" .tar.gz))"
}
backup_remote_list() {
local -a remote_paths=("$@")
if [[ ${#remote_paths[@]} -eq 0 ]]; then
log "No remote paths to backup; skipping snapshot."
return 0
fi
local unique_paths
unique_paths=$(printf '%s\n' "${remote_paths[@]}" | awk 'length && !seen[$0]++')
local trimmed_list=""
while IFS= read -r path; do
local trimmed="${path#/}"
[[ -z "$trimmed" ]] && continue
trimmed_list+="$trimmed"$'\n'
done <<< "$unique_paths"
if [[ -z "$trimmed_list" ]]; then
log "No existing remote files detected to backup."
return 0
fi
local backup_id
backup_id=$(date +%Y%m%d_%H%M%S)
local backup_file="$BACKUP_DIR/$backup_id.tar.gz"
local list_file="$BACKUP_DIR/$backup_id.list"
remote_exec "mkdir -p '$BACKUP_DIR'"
remote_exec "cat <<'EOF' > '$list_file'
$trimmed_list
EOF"
if remote_exec "cd / && tar --ignore-failed-read -czf '$backup_file' -T '$list_file' >/dev/null"; then
record_backup_metadata "$backup_file"
else
log "⚠️ Failed to create backup archive."
remote_exec "rm -f '$backup_file' '$list_file'"
fi
}
backup_remote_paths() {
local dir="$1"
local base="$2"
local -a files=()
gather_deploy_files "$dir" files
if [[ ${#files[@]} -eq 0 ]]; then
log "No local files detected for $dir; skipping backup."
return 0
fi
local -a remote_paths=()
for file in "${files[@]}"; do
local rel=${file#$dir/}
[[ -z "$rel" ]] && continue
remote_paths+=("$(join_path "$base" "$rel")")
done
backup_remote_list "${remote_paths[@]}"
}
ensure_remote_hash() {
if [[ -n "$REMOTE_HASH_CMD" ]]; then
return 0
@ -99,23 +188,8 @@ verify_remote() {
local base="$TARGET_PATH"
[[ "$FORCE_ROOT" == "true" ]] && base="/"
ensure_remote_hash || return
local -a candidates
if [[ ${#INCLUDE_PATHS[@]} -gt 0 ]]; then
for inc in "${INCLUDE_PATHS[@]}"; do
local path="$dir/$inc"
if [[ -d "$path" ]]; then
while IFS= read -r f; do
candidates+=("$f")
done < <(find "$path" -type f -not -path '*/.git/*' | sort)
elif [[ -f "$path" ]]; then
candidates+=("$path")
fi
done
else
while IFS= read -r f; do
candidates+=("$f")
done < <(find "$dir" -type f -not -path '*/.git/*' | sort)
fi
local -a candidates=()
gather_deploy_files "$dir" candidates
local -a samples
for f in "${candidates[@]}"; do
samples+=("$f")
@ -269,12 +343,34 @@ deploy_profile_theme() {
"luci-app-system-hub/htdocs/luci-static/resources/system-hub/dashboard.css:/www/luci-static/resources/system-hub/"
"luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js:/www/luci-static/resources/view/system-hub/"
)
remote_exec "mkdir -p /usr/libexec/rpcd /usr/share/rpcd/acl.d /www/luci-static/resources/secubox /www/luci-static/resources/view/secubox /www/luci-static/resources/system-hub /www/luci-static/resources/view/system-hub /www/luci-static/resources/secubox-theme"
local -a backup_targets=()
for entry in "${files[@]}"; do
local src=${entry%%:*}
local dest=${entry##*:}
local remote_file=$(join_path "$dest" "$(basename "$src")")
backup_targets+=("$remote_file")
done
backup_remote_list "${backup_targets[@]}"
for entry in "${files[@]}"; do
local src=${entry%%:*}
local dest=${entry##*:}
log "Copying $src -> $dest"
copy_file "$src" "$dest"
done
local theme_src_dir="luci-theme-secubox/htdocs/luci-static/resources"
if [[ -d "$theme_src_dir/secubox-theme" ]]; then
backup_remote_paths "$theme_src_dir/secubox-theme" "/www/luci-static/resources/secubox-theme"
local theme_archive=$(mktemp /tmp/secubox-theme-XXXX.tar.gz)
( cd "$theme_src_dir" && tar -czf "$theme_archive" secubox-theme/ )
local remote_theme="/tmp/secubox-theme.tar.gz"
log "Copying theme bundle to /www/luci-static/resources/secubox-theme"
copy_file "$theme_archive" "$remote_theme"
remote_exec "mkdir -p /www/luci-static/resources && tar -xzf '$remote_theme' -C /www/luci-static/resources && rm -f '$remote_theme'"
rm -f "$theme_archive"
fi
log "Setting permissions + restarting rpcd"
remote_exec "chmod +x /usr/libexec/rpcd/luci.secubox && \\
chmod 644 /www/luci-static/resources/secubox/*.{js,css} 2>/dev/null || true && \\
@ -331,6 +427,8 @@ while [[ $# -gt 0 ]]; do
MODE="apk"; PKG_PATH="$2"; shift 2 ;;
--src)
MODE="src"; SRC_PATH="$2"; shift 2 ;;
--src-clean)
MODE="src-clean"; SRC_PATH="$2"; shift 2 ;;
--src-select)
MODE="src"; SRC_PATH=""; shift ;;
--git)
@ -353,6 +451,13 @@ while [[ $# -gt 0 ]]; do
FORCE_ROOT="true"; shift ;;
--no-auto-profile)
AUTO_PROFILE=0; shift ;;
--uninstall)
MODE="uninstall"
if [[ $# -gt 1 && "$2" != --* ]]; then
UNINSTALL_TARGET="$2"; shift 2
else
UNINSTALL_TARGET="latest"; shift
fi ;;
-h|--help)
usage ;;
*)
@ -366,6 +471,10 @@ if [[ "$APP_NAME" == "list" ]]; then
APP_NAME=""
fi
if [[ "$MODE" == "uninstall" ]]; then
perform_uninstall "$UNINSTALL_TARGET"
fi
if [[ $LIST_APPS -eq 1 ]]; then
list_luci_apps
exit 0
@ -423,6 +532,13 @@ if [[ -z "$PROFILE" && "$MODE" == "src" && "$AUTO_PROFILE" -eq 1 && -n "$SRC_PAT
log "Auto-detected LuCI app at $SRC_PATH (use --no-auto-profile to disable)."
fi
if [[ "$MODE" == "src-clean" ]]; then
if [[ -z "$SRC_PATH" || ! -d "$SRC_PATH" ]]; then
echo "Error: --src-clean requires a valid --src path" >&2
exit 1
fi
fi
if [[ -z "$MODE" && -z "$PROFILE" ]]; then
echo "Error: specify one of --ipk/--apk/--src/--git or --profile" >&2
usage
@ -433,7 +549,7 @@ if [[ -n "$MODE" && -n "$PROFILE" ]]; then
exit 1
fi
if [[ "$FORCE_ROOT" == "true" && "$MODE" != "src" && "$PROFILE" != "luci-app" ]]; then
if [[ "$FORCE_ROOT" == "true" && "$MODE" != "src" && "$MODE" != "src-clean" && "$PROFILE" != "luci-app" ]]; then
echo "Error: --force-root is only valid with --src" >&2
exit 1
fi
@ -489,6 +605,7 @@ upload_source_dir() {
if [[ "$FORCE_ROOT" == "true" ]]; then
extract_target="/"
fi
backup_remote_paths "$dir" "$extract_target"
log "Extracting to $extract_target"
remote_exec "mkdir -p $extract_target && tar -xzf $remote -C $extract_target && rm -f $remote"
if [[ "$CACHE_BUST" -eq 1 ]]; then
@ -511,6 +628,79 @@ clone_and_upload() {
upload_source_dir "$cleanup_tmp"
}
clean_source_dir() {
local dir="$1"
if [[ -z "$dir" || ! -d "$dir" ]]; then
echo "Error: --src-clean requires a valid local directory" >&2
exit 1
fi
local base="$TARGET_PATH"
[[ "$FORCE_ROOT" == "true" ]] && base="/"
log "🧹 Removing files deployed from $dir (target base: $base)"
local -a files=()
gather_deploy_files "$dir" files
if [[ ${#files[@]} -eq 0 ]]; then
log "No files detected within $dir; nothing to remove."
return 0
fi
local list=""
for file in "${files[@]}"; do
local rel=${file#$dir/}
[[ -z "$rel" ]] && continue
local remote=$(join_path "$base" "$rel")
list+="$remote"$'\n'
done
remote_exec "cat <<'EOF' > /tmp/quickdeploy-clean.list
$list
EOF"
remote_exec "while IFS= read -r f || [ -n \"\$f\" ]; do [ -z \"\$f\" ] && continue; rm -f \"\$f\" 2>/dev/null || true; done < /tmp/quickdeploy-clean.list; rm -f /tmp/quickdeploy-clean.list"
if [[ "$CACHE_BUST" -eq 1 ]]; then
remote_exec "rm -rf /tmp/luci-*"
fi
log "Cleanup complete."
}
resolve_backup_file() {
local target="$1"
local backup_file=""
if [[ "$target" == "latest" || -z "$target" ]]; then
backup_file=$(remote_exec "ls -1t '$BACKUP_DIR'/*.tar.gz 2>/dev/null | head -n1") || true
else
if [[ "$target" == /* ]]; then
backup_file="$target"
else
backup_file="$BACKUP_DIR/$target"
[[ "$target" != *.tar.gz ]] && backup_file="$backup_file.tar.gz"
fi
fi
echo "$backup_file"
}
perform_uninstall() {
local target="$1"
local backup_file
backup_file=$(resolve_backup_file "$target")
if [[ -z "$backup_file" ]]; then
echo "No backup archives found in $BACKUP_DIR" >&2
exit 1
fi
local list_file="${backup_file%.tar.gz}.list"
log "Restoring from backup: $backup_file"
if ! remote_exec "if [ ! -f '$backup_file' ]; then echo 'Backup $backup_file not found' >&2; exit 1; fi"; then
exit 1
fi
if ! remote_exec "if [ ! -f '$list_file' ]; then echo 'Warning: manifest $list_file missing; proceeding without cleanup.' >&2; fi"; then
true
fi
remote_exec "if [ -f '$list_file' ]; then while IFS= read -r rel || [ -n \"\$rel\" ]; do [ -z \"\$rel\" ] && continue; rm -f \"/\$rel\" 2>/dev/null || true; done < '$list_file'; fi"
remote_exec "cd / && tar -xzf '$backup_file'"
if [[ "$CACHE_BUST" -eq 1 ]]; then
remote_exec "rm -rf /tmp/luci-*"
fi
log "Rollback complete."
exit 0
}
if [[ -n "$PROFILE" ]]; then
case "$PROFILE" in
theme|theme-system)
@ -541,6 +731,8 @@ else
install_apk "$PKG_PATH" ;;
src)
upload_source_dir "$SRC_PATH" ;;
src-clean)
clean_source_dir "$SRC_PATH" ;;
git)
clone_and_upload ;;
*)

View File

@ -1,218 +0,0 @@
#!/bin/sh
# SecuBox Apps CLI
# Lists/installs/removes plugin manifests declared under plugins/*/manifest.json
set -eu
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
DEFAULT_PLUGINS_DIR="/usr/share/secubox/plugins"
if [ -n "${SECUBOX_PLUGINS_DIR:-}" ]; then
PLUGINS_DIR="$SECUBOX_PLUGINS_DIR"
elif [ -d "$SCRIPT_DIR/../plugins" ]; then
PLUGINS_DIR=$(cd "$SCRIPT_DIR/../plugins" && pwd)
else
PLUGINS_DIR="$DEFAULT_PLUGINS_DIR"
fi
OPKG_UPDATED=0
info() { printf '[INFO] %s\n' "$*"; }
warn() { printf '[WARN] %s\n' "$*" >&2; }
err() { printf '[ERROR] %s\n' "$*" >&2; }
usage() {
cat <<'USAGE'
SecuBox Apps CLI
Usage: secubox-app <command> [arguments]
Commands:
list Show available app manifests
show <app-id> Display manifest details
install <app-id> Install required packages + run install action
remove <app-id> Remove packages listed in manifest
status <app-id> Show install state and run status action if defined
update <app-id> Run plugin update action or opkg upgrade
Environment:
SECUBOX_PLUGINS_DIR Override manifest directory (default: /usr/share/secubox/plugins)
USAGE
}
require_tool() {
command -v "$1" >/dev/null 2>&1 || { err "Missing dependency: $1"; exit 1; }
}
require_opkg() { require_tool opkg; }
require_jsonfilter() { require_tool jsonfilter; }
manifest_path() {
local id="$1"
local file="$PLUGINS_DIR/$id/manifest.json"
[ -f "$file" ] || { err "Manifest not found for '$id' ($file)"; exit 1; }
printf '%s' "$file"
}
manifest_field() {
local file="$1"; shift
jsonfilter -s "$file" -e "$1" 2>/dev/null || true
}
manifest_packages() {
local file="$1"
jsonfilter -s "$file" -e '@.packages[*]' 2>/dev/null || true
}
manifest_action() {
local file="$1"; shift
jsonfilter -s "$file" -e "@.actions.$1" 2>/dev/null || true
}
ensure_opkg_updated() {
[ "$OPKG_UPDATED" -eq 1 ] && return
opkg update >/dev/null
OPKG_UPDATED=1
}
packages_installed() {
local all_installed=0
local partial=0
local pkg
for pkg in $1; do
if opkg status "$pkg" >/dev/null 2>&1; then
continue
else
all_installed=1
fi
[ -n "$partial" ]
done
}
plugin_state() {
local file="$1"
local pkgs pkg missing=0 installed=0 total=0
pkgs=$(manifest_packages "$file")
for pkg in $pkgs; do
total=$((total+1))
if opkg status "$pkg" >/dev/null 2>&1; then
installed=$((installed+1))
else
missing=$((missing+1))
fi
done
if [ "$total" -eq 0 ]; then
echo "n/a"
elif [ "$missing" -eq 0 ]; then
echo "installed"
elif [ "$installed" -eq 0 ]; then
echo "missing"
else
echo "partial"
fi
}
list_plugins() {
require_jsonfilter
require_opkg
printf '%-16s %-22s %-10s %-10s\n' "ID" "Name" "Type" "State"
printf '%-16s %-22s %-10s %-10s\n' "--" "----" "----" "-----"
find "$PLUGINS_DIR" -mindepth 2 -maxdepth 2 -name manifest.json | sort | while read -r file; do
local id name type state
id=$(manifest_field "$file" '@.id')
name=$(manifest_field "$file" '@.name')
type=$(manifest_field "$file" '@.type')
state=$(plugin_state "$file")
printf '%-16s %-22s %-10s %-10s\n' "$id" "${name:-Unknown}" "${type:-?}" "$state"
done
}
show_manifest() {
require_jsonfilter
local file=$(manifest_path "$1")
cat "$file"
}
install_plugin() {
require_jsonfilter
require_opkg
local file=$(manifest_path "$1")
local pkgs pkg
pkgs=$(manifest_packages "$file")
if [ -z "$pkgs" ]; then
warn "Manifest has no packages; nothing to install."
else
for pkg in $pkgs; do
if opkg status "$pkg" >/dev/null 2>&1; then
info "$pkg already installed"
else
info "Installing $pkg"
ensure_opkg_updated
opkg install "$pkg"
fi
done
fi
local install_cmd
install_cmd=$(manifest_action "$file" install)
if [ -n "$install_cmd" ]; then
info "Running install action: $install_cmd"
sh -c "$install_cmd"
fi
}
remove_plugin() {
require_jsonfilter
require_opkg
local file=$(manifest_path "$1")
local pkgs pkg
pkgs=$(manifest_packages "$file")
for pkg in $pkgs; do
if opkg status "$pkg" >/dev/null 2>&1; then
info "Removing $pkg"
opkg remove "$pkg"
else
info "$pkg not installed"
fi
done
}
plugin_status_cmd() {
require_jsonfilter
local file=$(manifest_path "$1")
local status_cmd
status_cmd=$(manifest_action "$file" status)
local state
state=$(plugin_state "$file")
printf 'App: %s\nState: %s\n' "$1" "$state"
if [ -n "$status_cmd" ]; then
printf 'Status command output:\n'
sh -c "$status_cmd" || true
fi
}
update_plugin() {
require_jsonfilter
require_opkg
local file=$(manifest_path "$1")
local update_cmd pkgs pkg
update_cmd=$(manifest_action "$file" update)
if [ -n "$update_cmd" ]; then
info "Running update action: $update_cmd"
sh -c "$update_cmd"
else
pkgs=$(manifest_packages "$file")
for pkg in $pkgs; do
info "Upgrading $pkg"
ensure_opkg_updated
opkg upgrade "$pkg" || true
done
fi
}
case "${1:-}" in
list) shift; list_plugins ;;
show) shift; [ $# -ge 1 ] || { err "show requires an app id"; exit 1; }; show_manifest "$1" ;;
install) shift; [ $# -ge 1 ] || { err "install requires an app id"; exit 1; }; install_plugin "$1" ;;
remove) shift; [ $# -ge 1 ] || { err "remove requires an app id"; exit 1; }; remove_plugin "$1" ;;
status) shift; [ $# -ge 1 ] || { err "status requires an app id"; exit 1; }; plugin_status_cmd "$1" ;;
update) shift; [ $# -ge 1 ] || { err "update requires an app id"; exit 1; }; update_plugin "$1" ;;
help|--help|-h|'') usage ;;
*) err "Unknown command: $1"; usage; exit 1 ;;
esac

1
secubox-tools/secubox-app Symbolic link
View File

@ -0,0 +1 @@
../package/secubox/secubox-app/files/usr/sbin/secubox-app