refactor secubox app packaging and theme
This commit is contained in:
parent
06ac101e5a
commit
92eff5aad7
@ -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.
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
@ -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(),
|
||||
|
||||
@ -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'),
|
||||
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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); }
|
||||
@ -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; }
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
37
package/secubox/secubox-app/Makefile
Normal file
37
package/secubox/secubox-app/Makefile
Normal 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))
|
||||
253
package/secubox/secubox-app/files/usr/sbin/secubox-app
Executable file
253
package/secubox/secubox-app/files/usr/sbin/secubox-app
Executable 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
1
plugins
Symbolic link
@ -0,0 +1 @@
|
||||
package/secubox/secubox-app/files/usr/share/secubox/plugins
|
||||
1
secubox-app-domoticz
Symbolic link
1
secubox-app-domoticz
Symbolic link
@ -0,0 +1 @@
|
||||
package/secubox/secubox-app-domoticz
|
||||
1
secubox-app-lyrion
Symbolic link
1
secubox-app-lyrion
Symbolic link
@ -0,0 +1 @@
|
||||
package/secubox/secubox-app-lyrion
|
||||
1
secubox-app-zigbee2mqtt
Symbolic link
1
secubox-app-zigbee2mqtt
Symbolic link
@ -0,0 +1 @@
|
||||
package/secubox/secubox-app-zigbee2mqtt
|
||||
@ -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"
|
||||
|
||||
@ -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 ;;
|
||||
*)
|
||||
|
||||
@ -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
1
secubox-tools/secubox-app
Symbolic link
@ -0,0 +1 @@
|
||||
../package/secubox/secubox-app/files/usr/sbin/secubox-app
|
||||
Loading…
Reference in New Issue
Block a user