feat(magicmirror2): Add MagicMirror² and MMPM packages
New packages: - secubox-app-magicmirror2 (0.4.0): MagicMirror² smart display platform - LXC container with Docker image extraction - mm2ctl CLI for management - Support for gzip/zstd compressed layers - Default port 8082 - luci-app-magicmirror2 (0.4.0): LuCI web interface - Dashboard, modules, webui, settings views - RPCD backend for service control - Module management integration - secubox-app-mmpm (0.2.0): MMPM package manager - Installs MMPM in MagicMirror2 container - mmpmctl CLI for module management - Web GUI on port 7891 - luci-app-mmpm (0.2.0): LuCI interface for MMPM - Dashboard with install/update controls - Module search and management - Embedded web GUI view Portal integration: - Added MagicMirror² and MMPM to Services section - Portal version bumped to 0.6.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
447e4ab2be
commit
a83cde0885
63
package/secubox/luci-app-magicmirror2/Makefile
Normal file
63
package/secubox/luci-app-magicmirror2/Makefile
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
# Copyright (C) 2026 CyberMind.fr
|
||||||
|
#
|
||||||
|
# LuCI MagicMirror2 Dashboard - Smart Display Platform Interface
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=luci-app-magicmirror2
|
||||||
|
PKG_VERSION:=0.4.0
|
||||||
|
PKG_RELEASE:=5
|
||||||
|
PKG_ARCH:=all
|
||||||
|
|
||||||
|
PKG_LICENSE:=Apache-2.0
|
||||||
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI MagicMirror2 Dashboard
|
||||||
|
LUCI_DESCRIPTION:=Modern dashboard for MagicMirror2 smart display platform with module manager and SecuBox theme
|
||||||
|
LUCI_DEPENDS:=+luci-base +luci-app-secubox +secubox-app-magicmirror2 +jq
|
||||||
|
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/conffiles
|
||||||
|
/etc/config/magicmirror2
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/install
|
||||||
|
# RPCD backend (MUST be 755 for ubus calls)
|
||||||
|
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||||
|
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.magicmirror2 $(1)/usr/libexec/rpcd/
|
||||||
|
|
||||||
|
# ACL permissions
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
|
||||||
|
|
||||||
|
# LuCI menu
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
|
||||||
|
|
||||||
|
# JavaScript resources
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/magicmirror2
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/magicmirror2/*.js $(1)/www/luci-static/resources/magicmirror2/ 2>/dev/null || true
|
||||||
|
|
||||||
|
# JavaScript views
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/magicmirror2
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/magicmirror2/*.js $(1)/www/luci-static/resources/view/magicmirror2/
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/postinst
|
||||||
|
#!/bin/sh
|
||||||
|
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||||
|
# Restart RPCD to register new methods
|
||||||
|
/etc/init.d/rpcd restart
|
||||||
|
rm -rf /tmp/luci-modulecache /tmp/luci-indexcache 2>/dev/null
|
||||||
|
echo "MagicMirror2 Dashboard installed."
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
endef
|
||||||
|
|
||||||
|
# call BuildPackage - OpenWrt buildroot
|
||||||
|
$(eval $(call BuildPackage,luci-app-magicmirror2))
|
||||||
@ -0,0 +1,159 @@
|
|||||||
|
'use strict';
|
||||||
|
'require baseclass';
|
||||||
|
'require rpc';
|
||||||
|
|
||||||
|
var callGetStatus = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'get_status',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetConfig = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'get_config',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetDisplayConfig = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'get_display_config',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetWeatherConfig = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'get_weather_config',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetModulesConfig = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'get_modules_config',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetInstalledModules = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'get_installed_modules',
|
||||||
|
expect: { modules: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetWebUrl = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'get_web_url',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callServiceStart = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'service_start',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callServiceStop = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'service_stop',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callServiceRestart = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'service_restart',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callInstallModule = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'install_module',
|
||||||
|
params: ['name'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callRemoveModule = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'remove_module',
|
||||||
|
params: ['name'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callUpdateModules = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'update_modules',
|
||||||
|
params: ['name'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callRegenerateConfig = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'regenerate_config',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callSetConfig = rpc.declare({
|
||||||
|
object: 'luci.magicmirror2',
|
||||||
|
method: 'set_config',
|
||||||
|
params: ['key', 'value'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
return baseclass.extend({
|
||||||
|
getStatus: function() {
|
||||||
|
return callGetStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
getConfig: function() {
|
||||||
|
return callGetConfig();
|
||||||
|
},
|
||||||
|
|
||||||
|
getDisplayConfig: function() {
|
||||||
|
return callGetDisplayConfig();
|
||||||
|
},
|
||||||
|
|
||||||
|
getWeatherConfig: function() {
|
||||||
|
return callGetWeatherConfig();
|
||||||
|
},
|
||||||
|
|
||||||
|
getModulesConfig: function() {
|
||||||
|
return callGetModulesConfig();
|
||||||
|
},
|
||||||
|
|
||||||
|
getInstalledModules: function() {
|
||||||
|
return callGetInstalledModules();
|
||||||
|
},
|
||||||
|
|
||||||
|
getWebUrl: function() {
|
||||||
|
return callGetWebUrl();
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceStart: function() {
|
||||||
|
return callServiceStart();
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceStop: function() {
|
||||||
|
return callServiceStop();
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceRestart: function() {
|
||||||
|
return callServiceRestart();
|
||||||
|
},
|
||||||
|
|
||||||
|
installModule: function(name) {
|
||||||
|
return callInstallModule(name);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeModule: function(name) {
|
||||||
|
return callRemoveModule(name);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateModules: function(name) {
|
||||||
|
return callUpdateModules(name || '');
|
||||||
|
},
|
||||||
|
|
||||||
|
regenerateConfig: function() {
|
||||||
|
return callRegenerateConfig();
|
||||||
|
},
|
||||||
|
|
||||||
|
setConfig: function(key, value) {
|
||||||
|
return callSetConfig(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,259 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require poll';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require magicmirror2.api as api';
|
||||||
|
'require secubox-theme/theme as Theme';
|
||||||
|
'require secubox-portal/header as SbHeader';
|
||||||
|
|
||||||
|
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||||
|
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||||
|
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||||
|
Theme.init({ language: lang });
|
||||||
|
|
||||||
|
var MM2_NAV = [
|
||||||
|
{ id: 'dashboard', icon: '📊', label: 'Dashboard' },
|
||||||
|
{ id: 'webui', icon: '🖥️', label: 'Display' },
|
||||||
|
{ id: 'modules', icon: '🧩', label: 'Modules' },
|
||||||
|
{ id: 'settings', icon: '⚙️', label: 'Settings' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderMM2Nav(activeId) {
|
||||||
|
return E('div', {
|
||||||
|
'class': 'mm2-app-nav',
|
||||||
|
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;'
|
||||||
|
}, MM2_NAV.map(function(item) {
|
||||||
|
var isActive = activeId === item.id;
|
||||||
|
return E('a', {
|
||||||
|
'href': L.url('admin', 'secubox', 'services', 'magicmirror2', item.id),
|
||||||
|
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||||
|
(isActive ? 'background:linear-gradient(135deg,#9b59b6,#8e44ad);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||||
|
}, [
|
||||||
|
E('span', {}, item.icon),
|
||||||
|
E('span', {}, _(item.label))
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
title: _('MagicMirror2 Dashboard'),
|
||||||
|
pollInterval: 5,
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
api.getStatus(),
|
||||||
|
api.getConfig(),
|
||||||
|
api.getInstalledModules()
|
||||||
|
]).then(function(results) {
|
||||||
|
return {
|
||||||
|
status: results[0],
|
||||||
|
config: results[1],
|
||||||
|
modules: results[2]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var self = this;
|
||||||
|
var status = data.status || {};
|
||||||
|
var config = data.config || {};
|
||||||
|
var modules = (data.modules || {}).modules || [];
|
||||||
|
|
||||||
|
var view = E('div', { 'class': 'mm2-dashboard' }, [
|
||||||
|
E('style', {}, [
|
||||||
|
':root { --mm2-primary: #9b59b6; --mm2-success: #27ae60; --mm2-warning: #f39c12; --mm2-danger: #e74c3c; --mm2-bg-card: #141419; --mm2-text: #fff; --mm2-text-muted: #a0a0b0; }',
|
||||||
|
'.mm2-dashboard { color: var(--mm2-text); }',
|
||||||
|
'.mm2-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; padding: 20px 24px; background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%); border-radius: 16px; }',
|
||||||
|
'.mm2-logo { display: flex; align-items: center; gap: 16px; }',
|
||||||
|
'.mm2-logo-icon { font-size: 48px; }',
|
||||||
|
'.mm2-logo-text { font-size: 28px; font-weight: 700; }',
|
||||||
|
'.mm2-logo-sub { font-size: 14px; opacity: 0.8; }',
|
||||||
|
'.mm2-status-badge { display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 20px; font-weight: 500; }',
|
||||||
|
'.mm2-status-badge.running { background: rgba(39, 174, 96, 0.2); color: #27ae60; }',
|
||||||
|
'.mm2-status-badge.stopped { background: rgba(231, 76, 60, 0.2); color: #e74c3c; }',
|
||||||
|
'.mm2-status-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }',
|
||||||
|
'.mm2-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 24px; }',
|
||||||
|
'.mm2-card { background: var(--mm2-bg-card); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 20px; }',
|
||||||
|
'.mm2-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }',
|
||||||
|
'.mm2-card-title { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 600; }',
|
||||||
|
'.mm2-card-title-icon { font-size: 20px; }',
|
||||||
|
'.mm2-stat { text-align: center; padding: 16px; }',
|
||||||
|
'.mm2-stat-value { font-size: 36px; font-weight: 700; color: var(--mm2-primary); }',
|
||||||
|
'.mm2-stat-label { font-size: 14px; color: var(--mm2-text-muted); margin-top: 4px; }',
|
||||||
|
'.mm2-btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }',
|
||||||
|
'.mm2-btn-primary { background: linear-gradient(135deg, #9b59b6, #8e44ad); color: white; }',
|
||||||
|
'.mm2-btn-success { background: linear-gradient(135deg, #27ae60, #229954); color: white; }',
|
||||||
|
'.mm2-btn-danger { background: linear-gradient(135deg, #e74c3c, #c0392b); color: white; }',
|
||||||
|
'.mm2-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }',
|
||||||
|
'.mm2-actions { display: flex; gap: 12px; flex-wrap: wrap; }',
|
||||||
|
'.mm2-module-list { max-height: 300px; overflow-y: auto; }',
|
||||||
|
'.mm2-module-item { display: flex; align-items: center; gap: 12px; padding: 12px; background: rgba(255,255,255,0.02); border-radius: 8px; margin-bottom: 8px; }',
|
||||||
|
'.mm2-module-icon { font-size: 24px; }',
|
||||||
|
'.mm2-module-info { flex: 1; }',
|
||||||
|
'.mm2-module-name { font-weight: 500; }',
|
||||||
|
'.mm2-module-version { font-size: 12px; color: var(--mm2-text-muted); }',
|
||||||
|
'.mm2-quick-links a { display: block; padding: 12px 16px; background: rgba(255,255,255,0.02); border-radius: 8px; margin-bottom: 8px; color: var(--mm2-text); text-decoration: none; transition: background 0.2s; }',
|
||||||
|
'.mm2-quick-links a:hover { background: rgba(255,255,255,0.05); }'
|
||||||
|
].join('')),
|
||||||
|
|
||||||
|
// Header
|
||||||
|
E('div', { 'class': 'mm2-header' }, [
|
||||||
|
E('div', { 'class': 'mm2-logo' }, [
|
||||||
|
E('div', { 'class': 'mm2-logo-icon' }, '🪞'),
|
||||||
|
E('div', {}, [
|
||||||
|
E('div', { 'class': 'mm2-logo-text' }, 'MagicMirror²'),
|
||||||
|
E('div', { 'class': 'mm2-logo-sub' }, _('Smart Display Platform'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', {}, [
|
||||||
|
E('div', {
|
||||||
|
'class': 'mm2-status-badge ' + (status.running ? 'running' : 'stopped')
|
||||||
|
}, [
|
||||||
|
E('span', { 'class': 'mm2-status-dot' }),
|
||||||
|
status.running ? _('Running') : _('Stopped')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Stats Grid
|
||||||
|
E('div', { 'class': 'mm2-grid' }, [
|
||||||
|
E('div', { 'class': 'mm2-card' }, [
|
||||||
|
E('div', { 'class': 'mm2-stat' }, [
|
||||||
|
E('div', { 'class': 'mm2-stat-value' }, modules.length),
|
||||||
|
E('div', { 'class': 'mm2-stat-label' }, _('Installed Modules'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'mm2-card' }, [
|
||||||
|
E('div', { 'class': 'mm2-stat' }, [
|
||||||
|
E('div', { 'class': 'mm2-stat-value' }, ':' + (config.port || 8082)),
|
||||||
|
E('div', { 'class': 'mm2-stat-label' }, _('Web Port'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'mm2-card' }, [
|
||||||
|
E('div', { 'class': 'mm2-stat' }, [
|
||||||
|
E('div', { 'class': 'mm2-stat-value' }, (config.language || 'en').toUpperCase()),
|
||||||
|
E('div', { 'class': 'mm2-stat-label' }, _('Language'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
E('div', { 'class': 'mm2-card', 'style': 'margin-bottom: 24px;' }, [
|
||||||
|
E('div', { 'class': 'mm2-card-header' }, [
|
||||||
|
E('div', { 'class': 'mm2-card-title' }, [
|
||||||
|
E('span', { 'class': 'mm2-card-title-icon' }, '🎮'),
|
||||||
|
_('Service Control')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'mm2-actions' }, [
|
||||||
|
status.running ?
|
||||||
|
E('button', {
|
||||||
|
'class': 'mm2-btn mm2-btn-danger',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal(_('Stopping...'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Stopping MagicMirror2...'))
|
||||||
|
]);
|
||||||
|
api.serviceStop().then(function() {
|
||||||
|
ui.hideModal();
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ['⏹', _('Stop')]) :
|
||||||
|
E('button', {
|
||||||
|
'class': 'mm2-btn mm2-btn-success',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal(_('Starting...'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Starting MagicMirror2...'))
|
||||||
|
]);
|
||||||
|
api.serviceStart().then(function() {
|
||||||
|
ui.hideModal();
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ['▶', _('Start')]),
|
||||||
|
E('button', {
|
||||||
|
'class': 'mm2-btn mm2-btn-primary',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal(_('Restarting...'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Restarting MagicMirror2...'))
|
||||||
|
]);
|
||||||
|
api.serviceRestart().then(function() {
|
||||||
|
ui.hideModal();
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ['🔄', _('Restart')]),
|
||||||
|
status.web_url ? E('a', {
|
||||||
|
'class': 'mm2-btn mm2-btn-primary',
|
||||||
|
'href': status.web_url,
|
||||||
|
'target': '_blank'
|
||||||
|
}, ['🌐', _('Open Display')]) : null
|
||||||
|
].filter(Boolean))
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Two column layout
|
||||||
|
E('div', { 'style': 'display: grid; grid-template-columns: 2fr 1fr; gap: 20px;' }, [
|
||||||
|
// Installed Modules
|
||||||
|
E('div', { 'class': 'mm2-card' }, [
|
||||||
|
E('div', { 'class': 'mm2-card-header' }, [
|
||||||
|
E('div', { 'class': 'mm2-card-title' }, [
|
||||||
|
E('span', { 'class': 'mm2-card-title-icon' }, '🧩'),
|
||||||
|
_('Installed Modules')
|
||||||
|
]),
|
||||||
|
E('a', {
|
||||||
|
'href': L.url('admin', 'secubox', 'services', 'magicmirror2', 'modules'),
|
||||||
|
'style': 'color: var(--mm2-primary); text-decoration: none; font-size: 14px;'
|
||||||
|
}, _('Manage') + ' →')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'mm2-module-list' },
|
||||||
|
modules.length > 0 ?
|
||||||
|
modules.slice(0, 8).map(function(mod) {
|
||||||
|
return E('div', { 'class': 'mm2-module-item' }, [
|
||||||
|
E('div', { 'class': 'mm2-module-icon' }, '📦'),
|
||||||
|
E('div', { 'class': 'mm2-module-info' }, [
|
||||||
|
E('div', { 'class': 'mm2-module-name' }, mod.name),
|
||||||
|
E('div', { 'class': 'mm2-module-version' }, 'v' + (mod.version || 'unknown'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}) :
|
||||||
|
E('div', { 'style': 'text-align: center; padding: 40px; color: var(--mm2-text-muted);' }, [
|
||||||
|
E('div', { 'style': 'font-size: 48px; margin-bottom: 12px;' }, '📭'),
|
||||||
|
E('div', {}, _('No modules installed')),
|
||||||
|
E('a', {
|
||||||
|
'href': L.url('admin', 'secubox', 'services', 'magicmirror2', 'modules'),
|
||||||
|
'style': 'color: var(--mm2-primary);'
|
||||||
|
}, _('Install modules'))
|
||||||
|
])
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Quick Links
|
||||||
|
E('div', { 'class': 'mm2-card' }, [
|
||||||
|
E('div', { 'class': 'mm2-card-header' }, [
|
||||||
|
E('div', { 'class': 'mm2-card-title' }, [
|
||||||
|
E('span', { 'class': 'mm2-card-title-icon' }, '🔗'),
|
||||||
|
_('Quick Links')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'mm2-quick-links' }, [
|
||||||
|
E('a', { 'href': 'https://docs.magicmirror.builders/', 'target': '_blank' }, '📚 ' + _('Documentation')),
|
||||||
|
E('a', { 'href': 'https://github.com/MagicMirrorOrg/MagicMirror', 'target': '_blank' }, '💻 ' + _('GitHub Repository')),
|
||||||
|
E('a', { 'href': 'https://forum.magicmirror.builders/', 'target': '_blank' }, '💬 ' + _('Community Forum')),
|
||||||
|
E('a', { 'href': 'https://github.com/MagicMirrorOrg/MagicMirror-3rd-Party-Modules', 'target': '_blank' }, '🧩 ' + _('Module Directory'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||||
|
wrapper.appendChild(SbHeader.render());
|
||||||
|
wrapper.appendChild(renderMM2Nav('dashboard'));
|
||||||
|
wrapper.appendChild(view);
|
||||||
|
return wrapper;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,290 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require magicmirror2.api as api';
|
||||||
|
'require secubox-theme/theme as Theme';
|
||||||
|
'require secubox-portal/header as SbHeader';
|
||||||
|
|
||||||
|
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||||
|
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||||
|
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||||
|
Theme.init({ language: lang });
|
||||||
|
|
||||||
|
var MM2_NAV = [
|
||||||
|
{ id: 'dashboard', icon: '📊', label: 'Dashboard' },
|
||||||
|
{ id: 'webui', icon: '🖥️', label: 'Display' },
|
||||||
|
{ id: 'modules', icon: '🧩', label: 'Modules' },
|
||||||
|
{ id: 'settings', icon: '⚙️', label: 'Settings' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderMM2Nav(activeId) {
|
||||||
|
return E('div', {
|
||||||
|
'class': 'mm2-app-nav',
|
||||||
|
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;'
|
||||||
|
}, MM2_NAV.map(function(item) {
|
||||||
|
var isActive = activeId === item.id;
|
||||||
|
return E('a', {
|
||||||
|
'href': L.url('admin', 'secubox', 'services', 'magicmirror2', item.id),
|
||||||
|
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||||
|
(isActive ? 'background:linear-gradient(135deg,#9b59b6,#8e44ad);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||||
|
}, [
|
||||||
|
E('span', {}, item.icon),
|
||||||
|
E('span', {}, _(item.label))
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popular modules list
|
||||||
|
var POPULAR_MODULES = [
|
||||||
|
{ name: 'MMM-WeatherChart', desc: 'Beautiful weather chart display', url: 'https://github.com/paphko/MMM-WeatherChart' },
|
||||||
|
{ name: 'MMM-Spotify', desc: 'Spotify now playing widget', url: 'https://github.com/skuethe/MMM-Spotify' },
|
||||||
|
{ name: 'MMM-GoogleCalendar', desc: 'Google Calendar integration', url: 'https://github.com/randomBraworker/MMM-GoogleCalendar' },
|
||||||
|
{ name: 'MMM-SystemStats', desc: 'System CPU, memory stats', url: 'https://github.com/BenRoe/MMM-SystemStats' },
|
||||||
|
{ name: 'MMM-NetworkScanner', desc: 'Network device scanner', url: 'https://github.com/ianperrin/MMM-NetworkScanner' },
|
||||||
|
{ name: 'MMM-PIR-Sensor', desc: 'Motion sensor support', url: 'https://github.com/paviro/MMM-PIR-Sensor' },
|
||||||
|
{ name: 'MMM-Face-Reco-DNN', desc: 'Face recognition profiles', url: 'https://github.com/nischi/MMM-Face-Reco-DNN' },
|
||||||
|
{ name: 'MMM-Remote-Control', desc: 'Remote control API', url: 'https://github.com/Jopyth/MMM-Remote-Control' },
|
||||||
|
{ name: 'MMM-MQTT', desc: 'MQTT integration', url: 'https://github.com/ottopaulsen/MMM-MQTT' },
|
||||||
|
{ name: 'MMM-Wallpaper', desc: 'Dynamic wallpapers', url: 'https://github.com/kolbyjack/MMM-Wallpaper' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
title: _('MagicMirror2 Modules'),
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
api.getStatus(),
|
||||||
|
api.getInstalledModules()
|
||||||
|
]).then(function(results) {
|
||||||
|
return {
|
||||||
|
status: results[0],
|
||||||
|
modules: results[1]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var self = this;
|
||||||
|
var status = data.status || {};
|
||||||
|
var modules = (data.modules || {}).modules || [];
|
||||||
|
|
||||||
|
var view = E('div', { 'class': 'mm2-modules' }, [
|
||||||
|
E('style', {}, [
|
||||||
|
':root { --mm2-primary: #9b59b6; --mm2-success: #27ae60; --mm2-warning: #f39c12; --mm2-danger: #e74c3c; --mm2-bg-card: #141419; --mm2-text: #fff; --mm2-text-muted: #a0a0b0; }',
|
||||||
|
'.mm2-modules { color: var(--mm2-text); }',
|
||||||
|
'.mm2-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }',
|
||||||
|
'.mm2-header h2 { margin: 0; display: flex; align-items: center; gap: 12px; }',
|
||||||
|
'.mm2-card { background: var(--mm2-bg-card); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 20px; margin-bottom: 20px; }',
|
||||||
|
'.mm2-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid rgba(255,255,255,0.08); }',
|
||||||
|
'.mm2-card-title { font-size: 18px; font-weight: 600; display: flex; align-items: center; gap: 8px; }',
|
||||||
|
'.mm2-btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }',
|
||||||
|
'.mm2-btn-primary { background: linear-gradient(135deg, #9b59b6, #8e44ad); color: white; }',
|
||||||
|
'.mm2-btn-success { background: linear-gradient(135deg, #27ae60, #229954); color: white; }',
|
||||||
|
'.mm2-btn-danger { background: rgba(231, 76, 60, 0.2); color: #e74c3c; border: 1px solid rgba(231, 76, 60, 0.3); }',
|
||||||
|
'.mm2-btn-sm { padding: 6px 12px; font-size: 12px; }',
|
||||||
|
'.mm2-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }',
|
||||||
|
'.mm2-module-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }',
|
||||||
|
'.mm2-module-card { background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; padding: 16px; transition: all 0.2s; }',
|
||||||
|
'.mm2-module-card:hover { background: rgba(255,255,255,0.04); border-color: rgba(155,89,182,0.3); }',
|
||||||
|
'.mm2-module-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }',
|
||||||
|
'.mm2-module-icon { font-size: 32px; }',
|
||||||
|
'.mm2-module-name { font-weight: 600; font-size: 15px; }',
|
||||||
|
'.mm2-module-version { font-size: 12px; color: var(--mm2-text-muted); }',
|
||||||
|
'.mm2-module-desc { font-size: 13px; color: var(--mm2-text-muted); margin-bottom: 12px; min-height: 40px; }',
|
||||||
|
'.mm2-module-actions { display: flex; gap: 8px; }',
|
||||||
|
'.mm2-install-form { display: flex; gap: 12px; margin-bottom: 20px; }',
|
||||||
|
'.mm2-install-form input { flex: 1; padding: 12px 16px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: white; font-size: 14px; }',
|
||||||
|
'.mm2-install-form input:focus { outline: none; border-color: var(--mm2-primary); }',
|
||||||
|
'.mm2-empty { text-align: center; padding: 60px 20px; }',
|
||||||
|
'.mm2-empty-icon { font-size: 64px; margin-bottom: 16px; }',
|
||||||
|
'.mm2-popular-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; }'
|
||||||
|
].join('')),
|
||||||
|
|
||||||
|
// Header
|
||||||
|
E('div', { 'class': 'mm2-header' }, [
|
||||||
|
E('h2', {}, ['🧩 ', _('Module Manager')]),
|
||||||
|
E('button', {
|
||||||
|
'class': 'mm2-btn mm2-btn-primary',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal(_('Updating...'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Updating all modules...'))
|
||||||
|
]);
|
||||||
|
api.updateModules().then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Modules updated successfully')), 'success');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, res.message || _('Update failed')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ['🔄', _('Update All')])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Install Module
|
||||||
|
E('div', { 'class': 'mm2-card' }, [
|
||||||
|
E('div', { 'class': 'mm2-card-header' }, [
|
||||||
|
E('div', { 'class': 'mm2-card-title' }, ['📥 ', _('Install Module')])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'mm2-install-form' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'id': 'mm2-module-input',
|
||||||
|
'placeholder': _('Module name (e.g., MMM-WeatherChart) or Git URL...')
|
||||||
|
}),
|
||||||
|
E('button', {
|
||||||
|
'class': 'mm2-btn mm2-btn-success',
|
||||||
|
'click': function() {
|
||||||
|
var input = document.getElementById('mm2-module-input');
|
||||||
|
var moduleName = input.value.trim();
|
||||||
|
if (!moduleName) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Please enter a module name or URL')), 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status.running) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Please start MagicMirror2 first')), 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.showModal(_('Installing...'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Installing module: ') + moduleName)
|
||||||
|
]);
|
||||||
|
|
||||||
|
api.installModule(moduleName).then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Module installed successfully')), 'success');
|
||||||
|
input.value = '';
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, res.message || _('Installation failed')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ['📥', _('Install')])
|
||||||
|
]),
|
||||||
|
E('p', { 'style': 'font-size: 13px; color: var(--mm2-text-muted); margin: 0;' },
|
||||||
|
_('Enter a module name like MMM-WeatherChart or a full Git URL'))
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Installed Modules
|
||||||
|
E('div', { 'class': 'mm2-card' }, [
|
||||||
|
E('div', { 'class': 'mm2-card-header' }, [
|
||||||
|
E('div', { 'class': 'mm2-card-title' }, ['📦 ', _('Installed Modules'), ' (' + modules.length + ')'])
|
||||||
|
]),
|
||||||
|
modules.length > 0 ?
|
||||||
|
E('div', { 'class': 'mm2-module-grid' },
|
||||||
|
modules.map(function(mod) {
|
||||||
|
return E('div', { 'class': 'mm2-module-card' }, [
|
||||||
|
E('div', { 'class': 'mm2-module-header' }, [
|
||||||
|
E('div', { 'class': 'mm2-module-icon' }, '📦'),
|
||||||
|
E('div', {}, [
|
||||||
|
E('div', { 'class': 'mm2-module-name' }, mod.name),
|
||||||
|
E('div', { 'class': 'mm2-module-version' }, 'v' + (mod.version || 'unknown'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'mm2-module-desc' }, mod.description || _('No description available')),
|
||||||
|
E('div', { 'class': 'mm2-module-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'mm2-btn mm2-btn-sm mm2-btn-primary',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal(_('Updating...'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Updating ') + mod.name)
|
||||||
|
]);
|
||||||
|
api.updateModules(mod.name).then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Module updated')), 'success');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, res.message), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, '🔄'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'mm2-btn mm2-btn-sm mm2-btn-danger',
|
||||||
|
'click': function() {
|
||||||
|
if (!confirm(_('Remove module ') + mod.name + '?')) return;
|
||||||
|
ui.showModal(_('Removing...'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Removing ') + mod.name)
|
||||||
|
]);
|
||||||
|
api.removeModule(mod.name).then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, res.message), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, '🗑')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
) :
|
||||||
|
E('div', { 'class': 'mm2-empty' }, [
|
||||||
|
E('div', { 'class': 'mm2-empty-icon' }, '📭'),
|
||||||
|
E('h3', {}, _('No modules installed')),
|
||||||
|
E('p', { 'style': 'color: var(--mm2-text-muted);' }, _('Install modules from the form above or from the popular list below'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Popular Modules
|
||||||
|
E('div', { 'class': 'mm2-card' }, [
|
||||||
|
E('div', { 'class': 'mm2-card-header' }, [
|
||||||
|
E('div', { 'class': 'mm2-card-title' }, ['⭐ ', _('Popular Modules')])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'mm2-popular-grid' },
|
||||||
|
POPULAR_MODULES.map(function(mod) {
|
||||||
|
var isInstalled = modules.some(function(m) { return m.name === mod.name; });
|
||||||
|
return E('div', { 'class': 'mm2-module-card' }, [
|
||||||
|
E('div', { 'class': 'mm2-module-header' }, [
|
||||||
|
E('div', { 'class': 'mm2-module-icon' }, '⭐'),
|
||||||
|
E('div', { 'class': 'mm2-module-name' }, mod.name)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'mm2-module-desc' }, mod.desc),
|
||||||
|
E('div', { 'class': 'mm2-module-actions' }, [
|
||||||
|
isInstalled ?
|
||||||
|
E('span', { 'style': 'color: var(--mm2-success); font-size: 13px;' }, '✓ ' + _('Installed')) :
|
||||||
|
E('button', {
|
||||||
|
'class': 'mm2-btn mm2-btn-sm mm2-btn-success',
|
||||||
|
'data-url': mod.url,
|
||||||
|
'click': function(ev) {
|
||||||
|
if (!status.running) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Please start MagicMirror2 first')), 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var url = ev.target.getAttribute('data-url');
|
||||||
|
ui.showModal(_('Installing...'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Installing ') + mod.name)
|
||||||
|
]);
|
||||||
|
api.installModule(url).then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, res.message), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ['📥', _('Install')])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||||
|
wrapper.appendChild(SbHeader.render());
|
||||||
|
wrapper.appendChild(renderMM2Nav('modules'));
|
||||||
|
wrapper.appendChild(view);
|
||||||
|
return wrapper;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,347 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require uci';
|
||||||
|
'require form';
|
||||||
|
'require magicmirror2.api as api';
|
||||||
|
'require secubox-theme/theme as Theme';
|
||||||
|
'require secubox-portal/header as SbHeader';
|
||||||
|
|
||||||
|
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||||
|
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||||
|
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||||
|
Theme.init({ language: lang });
|
||||||
|
|
||||||
|
var MM2_NAV = [
|
||||||
|
{ id: 'dashboard', icon: '📊', label: 'Dashboard' },
|
||||||
|
{ id: 'webui', icon: '🖥️', label: 'Display' },
|
||||||
|
{ id: 'modules', icon: '🧩', label: 'Modules' },
|
||||||
|
{ id: 'settings', icon: '⚙️', label: 'Settings' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderMM2Nav(activeId) {
|
||||||
|
return E('div', {
|
||||||
|
'class': 'mm2-app-nav',
|
||||||
|
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;'
|
||||||
|
}, MM2_NAV.map(function(item) {
|
||||||
|
var isActive = activeId === item.id;
|
||||||
|
return E('a', {
|
||||||
|
'href': L.url('admin', 'secubox', 'services', 'magicmirror2', item.id),
|
||||||
|
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||||
|
(isActive ? 'background:linear-gradient(135deg,#9b59b6,#8e44ad);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||||
|
}, [
|
||||||
|
E('span', {}, item.icon),
|
||||||
|
E('span', {}, _(item.label))
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
title: _('MagicMirror2 Settings'),
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
uci.load('magicmirror2'),
|
||||||
|
api.getStatus()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var self = this;
|
||||||
|
var status = data[1] || {};
|
||||||
|
|
||||||
|
var m, s, o;
|
||||||
|
|
||||||
|
m = new form.Map('magicmirror2', null, null);
|
||||||
|
m.tabbed = true;
|
||||||
|
|
||||||
|
// General Settings
|
||||||
|
s = m.section(form.NamedSection, 'main', 'magicmirror2', _('General Settings'));
|
||||||
|
s.anonymous = true;
|
||||||
|
s.addremove = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'enabled', _('Enable Service'));
|
||||||
|
o.rmempty = false;
|
||||||
|
o.default = '0';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'port', _('Web Port'));
|
||||||
|
o.datatype = 'port';
|
||||||
|
o.default = '8082';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'address', _('Listen Address'));
|
||||||
|
o.default = '0.0.0.0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'language', _('Language'));
|
||||||
|
o.value('en', 'English');
|
||||||
|
o.value('fr', 'Français');
|
||||||
|
o.value('de', 'Deutsch');
|
||||||
|
o.value('es', 'Español');
|
||||||
|
o.value('it', 'Italiano');
|
||||||
|
o.value('nl', 'Nederlands');
|
||||||
|
o.value('pt', 'Português');
|
||||||
|
o.value('ru', 'Русский');
|
||||||
|
o.value('zh', '中文');
|
||||||
|
o.value('ja', '日本語');
|
||||||
|
o.default = 'en';
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'timezone', _('Timezone'));
|
||||||
|
o.value('Europe/Paris', 'Europe/Paris');
|
||||||
|
o.value('Europe/London', 'Europe/London');
|
||||||
|
o.value('Europe/Berlin', 'Europe/Berlin');
|
||||||
|
o.value('Europe/Madrid', 'Europe/Madrid');
|
||||||
|
o.value('Europe/Rome', 'Europe/Rome');
|
||||||
|
o.value('Europe/Amsterdam', 'Europe/Amsterdam');
|
||||||
|
o.value('Europe/Brussels', 'Europe/Brussels');
|
||||||
|
o.value('America/New_York', 'America/New_York');
|
||||||
|
o.value('America/Los_Angeles', 'America/Los_Angeles');
|
||||||
|
o.value('America/Chicago', 'America/Chicago');
|
||||||
|
o.value('Asia/Tokyo', 'Asia/Tokyo');
|
||||||
|
o.value('Asia/Shanghai', 'Asia/Shanghai');
|
||||||
|
o.value('Australia/Sydney', 'Australia/Sydney');
|
||||||
|
o.default = 'Europe/Paris';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'data_path', _('Data Path'));
|
||||||
|
o.default = '/srv/magicmirror2';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'memory_limit', _('Memory Limit'));
|
||||||
|
o.default = '512M';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
// Display Settings
|
||||||
|
s = m.section(form.NamedSection, 'display', 'display', _('Display Settings'));
|
||||||
|
s.anonymous = true;
|
||||||
|
s.addremove = false;
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'units', _('Units'));
|
||||||
|
o.value('metric', _('Metric (°C, km)'));
|
||||||
|
o.value('imperial', _('Imperial (°F, miles)'));
|
||||||
|
o.default = 'metric';
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'time_format', _('Time Format'));
|
||||||
|
o.value('24', _('24 Hour'));
|
||||||
|
o.value('12', _('12 Hour'));
|
||||||
|
o.default = '24';
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'show_period', _('Show AM/PM'));
|
||||||
|
o.depends('time_format', '12');
|
||||||
|
o.default = '1';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'animation_speed', _('Animation Speed (ms)'));
|
||||||
|
o.datatype = 'uinteger';
|
||||||
|
o.default = '1000';
|
||||||
|
|
||||||
|
// Weather Settings
|
||||||
|
s = m.section(form.NamedSection, 'weather', 'weather', _('Weather Settings'));
|
||||||
|
s.anonymous = true;
|
||||||
|
s.addremove = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'weather_enabled', _('Enable Weather Module'), _('Display current weather on the mirror'));
|
||||||
|
o.default = '1';
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'weather_provider', _('Weather Provider'));
|
||||||
|
o.value('openweathermap', 'OpenWeatherMap');
|
||||||
|
o.value('weathergov', 'Weather.gov (US only)');
|
||||||
|
o.value('weatherbit', 'Weatherbit');
|
||||||
|
o.default = 'openweathermap';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'weather_api_key', _('API Key'));
|
||||||
|
o.password = true;
|
||||||
|
o.rmempty = true;
|
||||||
|
o.description = _('Get your free API key from openweathermap.org');
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'weather_location', _('Location'));
|
||||||
|
o.placeholder = 'Paris,FR';
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'weather_lat', _('Latitude'));
|
||||||
|
o.datatype = 'float';
|
||||||
|
o.placeholder = '48.8566';
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'weather_lon', _('Longitude'));
|
||||||
|
o.datatype = 'float';
|
||||||
|
o.placeholder = '2.3522';
|
||||||
|
o.rmempty = true;
|
||||||
|
|
||||||
|
// Calendar Settings
|
||||||
|
s = m.section(form.NamedSection, 'calendar', 'calendar', _('Calendar Settings'));
|
||||||
|
s.anonymous = true;
|
||||||
|
s.addremove = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'calendar_enabled', _('Enable Calendar Module'));
|
||||||
|
o.default = '1';
|
||||||
|
|
||||||
|
o = s.option(form.DynamicList, 'calendar_urls', _('Calendar URLs (iCal)'));
|
||||||
|
o.rmempty = true;
|
||||||
|
o.placeholder = 'https://calendar.google.com/calendar/ical/.../basic.ics';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'calendar_max_entries', _('Maximum Entries'));
|
||||||
|
o.datatype = 'uinteger';
|
||||||
|
o.default = '10';
|
||||||
|
|
||||||
|
// News Feed Settings
|
||||||
|
s = m.section(form.NamedSection, 'newsfeed', 'newsfeed', _('News Feed Settings'));
|
||||||
|
s.anonymous = true;
|
||||||
|
s.addremove = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'newsfeed_enabled', _('Enable News Feed Module'));
|
||||||
|
o.default = '1';
|
||||||
|
|
||||||
|
o = s.option(form.DynamicList, 'newsfeed_urls', _('RSS Feed URLs'));
|
||||||
|
o.rmempty = true;
|
||||||
|
o.placeholder = 'https://www.nytimes.com/svc/collections/v1/publish/https://www.nytimes.com/section/world/rss.xml';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'newsfeed_max_entries', _('Maximum Entries'));
|
||||||
|
o.datatype = 'uinteger';
|
||||||
|
o.default = '5';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'newsfeed_update_interval', _('Update Interval (ms)'));
|
||||||
|
o.datatype = 'uinteger';
|
||||||
|
o.default = '10000';
|
||||||
|
|
||||||
|
// Clock Settings
|
||||||
|
s = m.section(form.NamedSection, 'clock', 'clock', _('Clock Settings'));
|
||||||
|
s.anonymous = true;
|
||||||
|
s.addremove = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'clock_enabled', _('Enable Clock Module'));
|
||||||
|
o.default = '1';
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'clock_show_date', _('Show Date'));
|
||||||
|
o.default = '1';
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'clock_show_week', _('Show Week Number'));
|
||||||
|
o.default = '0';
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'clock_show_seconds', _('Show Seconds'));
|
||||||
|
o.default = '1';
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'clock_display_type', _('Display Type'));
|
||||||
|
o.value('digital', _('Digital'));
|
||||||
|
o.value('analog', _('Analog'));
|
||||||
|
o.value('both', _('Both'));
|
||||||
|
o.default = 'digital';
|
||||||
|
|
||||||
|
// Compliments Settings
|
||||||
|
s = m.section(form.NamedSection, 'compliments', 'compliments', _('Compliments Settings'));
|
||||||
|
s.anonymous = true;
|
||||||
|
s.addremove = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'compliments_enabled', _('Enable Compliments Module'));
|
||||||
|
o.default = '1';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'compliments_update_interval', _('Update Interval (ms)'));
|
||||||
|
o.datatype = 'uinteger';
|
||||||
|
o.default = '30000';
|
||||||
|
|
||||||
|
o = s.option(form.DynamicList, 'compliments_morning', _('Morning Compliments'));
|
||||||
|
o.rmempty = true;
|
||||||
|
o.placeholder = 'Good morning, sunshine!';
|
||||||
|
|
||||||
|
o = s.option(form.DynamicList, 'compliments_afternoon', _('Afternoon Compliments'));
|
||||||
|
o.rmempty = true;
|
||||||
|
o.placeholder = 'Looking good today!';
|
||||||
|
|
||||||
|
o = s.option(form.DynamicList, 'compliments_evening', _('Evening Compliments'));
|
||||||
|
o.rmempty = true;
|
||||||
|
o.placeholder = 'Wow, you look fantastic!';
|
||||||
|
|
||||||
|
var formRender = m.render();
|
||||||
|
|
||||||
|
return formRender.then(function(formEl) {
|
||||||
|
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||||
|
wrapper.appendChild(SbHeader.render());
|
||||||
|
wrapper.appendChild(renderMM2Nav('settings'));
|
||||||
|
|
||||||
|
// Add custom styles for form
|
||||||
|
wrapper.appendChild(E('style', {}, [
|
||||||
|
'.cbi-map { background: #141419; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 20px; }',
|
||||||
|
'.cbi-section { margin-bottom: 20px; }',
|
||||||
|
'.cbi-section-node { background: rgba(255,255,255,0.02); border-radius: 8px; padding: 16px; }',
|
||||||
|
'.cbi-tabmenu { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }',
|
||||||
|
'.cbi-tabmenu li { list-style: none; }',
|
||||||
|
'.cbi-tabmenu li a { display: block; padding: 10px 20px; background: rgba(255,255,255,0.05); border-radius: 8px; color: #a0a0b0; text-decoration: none; font-weight: 500; }',
|
||||||
|
'.cbi-tabmenu li.cbi-tab a { background: linear-gradient(135deg, #9b59b6, #8e44ad); color: white; }',
|
||||||
|
'.cbi-value { display: flex; align-items: center; padding: 12px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }',
|
||||||
|
'.cbi-value:last-child { border-bottom: none; }',
|
||||||
|
'.cbi-value-title { flex: 0 0 200px; font-weight: 500; color: #fff; }',
|
||||||
|
'.cbi-value-field { flex: 1; }',
|
||||||
|
'.cbi-value-description { font-size: 12px; color: #a0a0b0; margin-top: 4px; }',
|
||||||
|
'.cbi-input-text, .cbi-input-select, .cbi-input-password { width: 100%; padding: 10px 14px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; color: white; font-size: 14px; }',
|
||||||
|
'.cbi-input-text:focus, .cbi-input-select:focus, .cbi-input-password:focus { outline: none; border-color: #9b59b6; }',
|
||||||
|
'.cbi-button { padding: 10px 20px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }',
|
||||||
|
'.cbi-button-save, .cbi-button-apply { background: linear-gradient(135deg, #27ae60, #229954); color: white; }',
|
||||||
|
'.cbi-button-reset { background: rgba(255,255,255,0.1); color: white; }',
|
||||||
|
'.cbi-checkbox { accent-color: #9b59b6; }',
|
||||||
|
'.cbi-dynlist { display: flex; flex-direction: column; gap: 8px; }',
|
||||||
|
'.cbi-dynlist > .item { display: flex; gap: 8px; align-items: center; }',
|
||||||
|
'.cbi-dynlist > .item > input { flex: 1; }'
|
||||||
|
].join('')));
|
||||||
|
|
||||||
|
// Regenerate config button
|
||||||
|
var actionBar = E('div', {
|
||||||
|
'style': 'display: flex; gap: 12px; margin-bottom: 20px; padding: 16px; background: #141419; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px;'
|
||||||
|
}, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'style': 'background: linear-gradient(135deg, #9b59b6, #8e44ad); color: white;',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal(_('Regenerating...'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Regenerating MagicMirror2 config.js...'))
|
||||||
|
]);
|
||||||
|
api.regenerateConfig().then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Configuration regenerated. Restart the service to apply changes.')), 'success');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, res.message || _('Failed to regenerate config')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ['🔧 ', _('Regenerate Config')]),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'style': 'background: linear-gradient(135deg, #3498db, #2980b9); color: white;',
|
||||||
|
'click': function() {
|
||||||
|
if (!status.running) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Service is not running')), 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ui.showModal(_('Restarting...'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Restarting MagicMirror2...'))
|
||||||
|
]);
|
||||||
|
api.serviceRestart().then(function() {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Service restarted')), 'success');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ['🔄 ', _('Restart Service')])
|
||||||
|
]);
|
||||||
|
|
||||||
|
wrapper.appendChild(actionBar);
|
||||||
|
wrapper.appendChild(formEl);
|
||||||
|
return wrapper;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSave: function(ev) {
|
||||||
|
var map = document.querySelector('.cbi-map');
|
||||||
|
return dom.callClassMethod(map, 'save').then(function() {
|
||||||
|
return uci.save();
|
||||||
|
}).then(function() {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Settings saved. Click "Regenerate Config" to apply changes.')), 'info');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: function(ev) {
|
||||||
|
return this.handleSave(ev).then(function() {
|
||||||
|
return api.regenerateConfig();
|
||||||
|
}).then(function() {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Settings saved and config regenerated.')), 'success');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require magicmirror2.api as api';
|
||||||
|
'require secubox-theme/theme as Theme';
|
||||||
|
'require secubox-portal/header as SbHeader';
|
||||||
|
|
||||||
|
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||||
|
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||||
|
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||||
|
Theme.init({ language: lang });
|
||||||
|
|
||||||
|
var MM2_NAV = [
|
||||||
|
{ id: 'dashboard', icon: '📊', label: 'Dashboard' },
|
||||||
|
{ id: 'webui', icon: '🖥️', label: 'Display' },
|
||||||
|
{ id: 'modules', icon: '🧩', label: 'Modules' },
|
||||||
|
{ id: 'settings', icon: '⚙️', label: 'Settings' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderMM2Nav(activeId) {
|
||||||
|
return E('div', {
|
||||||
|
'class': 'mm2-app-nav',
|
||||||
|
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;'
|
||||||
|
}, MM2_NAV.map(function(item) {
|
||||||
|
var isActive = activeId === item.id;
|
||||||
|
return E('a', {
|
||||||
|
'href': L.url('admin', 'secubox', 'services', 'magicmirror2', item.id),
|
||||||
|
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||||
|
(isActive ? 'background:linear-gradient(135deg,#9b59b6,#8e44ad);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||||
|
}, [
|
||||||
|
E('span', {}, item.icon),
|
||||||
|
E('span', {}, _(item.label))
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
title: _('MagicMirror2 Display'),
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
api.getStatus(),
|
||||||
|
api.getWebUrl()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var status = data[0] || {};
|
||||||
|
var webData = data[1] || {};
|
||||||
|
|
||||||
|
var content;
|
||||||
|
|
||||||
|
if (!status.running) {
|
||||||
|
content = E('div', {
|
||||||
|
'class': 'mm2-card',
|
||||||
|
'style': 'text-align: center; padding: 60px 20px; background: #141419; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px;'
|
||||||
|
}, [
|
||||||
|
E('div', { 'style': 'font-size: 64px; margin-bottom: 20px;' }, '⚠️'),
|
||||||
|
E('h2', { 'style': 'margin: 0 0 10px 0; color: #f39c12;' }, _('MagicMirror2 is not running')),
|
||||||
|
E('p', { 'style': 'color: #a0a0b0; margin: 0 0 20px 0;' }, _('Start the service to view the display')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'mm2-btn mm2-btn-success',
|
||||||
|
'style': 'display: inline-flex; align-items: center; gap: 8px; padding: 12px 24px; background: linear-gradient(135deg, #27ae60, #229954); color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer;',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal(_('Starting...'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Starting MagicMirror2...'))
|
||||||
|
]);
|
||||||
|
api.serviceStart().then(function() {
|
||||||
|
ui.hideModal();
|
||||||
|
setTimeout(function() { location.reload(); }, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ['▶ ', _('Start MagicMirror2')])
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
var iframeSrc = webData.web_url;
|
||||||
|
|
||||||
|
content = E('div', { 'style': 'display: flex; flex-direction: column; height: calc(100vh - 200px); min-height: 600px;' }, [
|
||||||
|
// Toolbar
|
||||||
|
E('div', {
|
||||||
|
'style': 'display: flex; align-items: center; gap: 12px; margin-bottom: 12px; padding: 12px 16px; background: #141419; border-radius: 8px; border: 1px solid rgba(255,255,255,0.08);'
|
||||||
|
}, [
|
||||||
|
E('span', { 'style': 'color: #27ae60; font-weight: 500;' }, '● ' + _('Live Preview')),
|
||||||
|
E('span', { 'style': 'color: #a0a0b0; font-size: 13px;' }, iframeSrc),
|
||||||
|
E('div', { 'style': 'flex: 1;' }),
|
||||||
|
E('button', {
|
||||||
|
'style': 'display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; background: rgba(255,255,255,0.1); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px;',
|
||||||
|
'click': function() {
|
||||||
|
var iframe = document.querySelector('.mm2-iframe');
|
||||||
|
if (iframe) iframe.src = iframe.src;
|
||||||
|
}
|
||||||
|
}, ['🔄 ', _('Refresh')]),
|
||||||
|
E('a', {
|
||||||
|
'style': 'display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; background: linear-gradient(135deg, #9b59b6, #8e44ad); color: white; border: none; border-radius: 6px; text-decoration: none; font-size: 13px;',
|
||||||
|
'href': iframeSrc,
|
||||||
|
'target': '_blank'
|
||||||
|
}, ['↗ ', _('Fullscreen')])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Iframe container
|
||||||
|
E('div', {
|
||||||
|
'style': 'flex: 1; border-radius: 12px; overflow: hidden; border: 1px solid rgba(255,255,255,0.1); background: #000;'
|
||||||
|
}, [
|
||||||
|
E('iframe', {
|
||||||
|
'class': 'mm2-iframe',
|
||||||
|
'src': iframeSrc,
|
||||||
|
'style': 'width: 100%; height: 100%; border: none;',
|
||||||
|
'allow': 'fullscreen'
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||||
|
wrapper.appendChild(SbHeader.render());
|
||||||
|
wrapper.appendChild(renderMM2Nav('webui'));
|
||||||
|
wrapper.appendChild(content);
|
||||||
|
return wrapper;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,427 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# RPCD backend for MagicMirror2 LuCI interface
|
||||||
|
# Copyright (C) 2026 CyberMind.fr (SecuBox)
|
||||||
|
#
|
||||||
|
|
||||||
|
. /lib/functions.sh
|
||||||
|
|
||||||
|
DATA_DIR=$(uci -q get magicmirror2.main.data_path || echo "/srv/magicmirror2")
|
||||||
|
LXC_NAME="magicmirror2"
|
||||||
|
|
||||||
|
# Get service status
|
||||||
|
get_status() {
|
||||||
|
local running=0
|
||||||
|
local pid=""
|
||||||
|
local lxc_state=""
|
||||||
|
local web_url=""
|
||||||
|
|
||||||
|
# Check LXC container status
|
||||||
|
if command -v lxc-info >/dev/null 2>&1; then
|
||||||
|
lxc_state=$(lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -oE 'RUNNING|STOPPED' || echo "UNKNOWN")
|
||||||
|
if [ "$lxc_state" = "RUNNING" ]; then
|
||||||
|
running=1
|
||||||
|
pid=$(lxc-info -n "$LXC_NAME" -p 2>/dev/null | grep -oE '[0-9]+' || echo "0")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local enabled=$(uci -q get magicmirror2.main.enabled || echo "0")
|
||||||
|
local port=$(uci -q get magicmirror2.main.port || echo "8082")
|
||||||
|
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||||
|
|
||||||
|
[ "$running" = "1" ] && web_url="http://${router_ip}:${port}"
|
||||||
|
|
||||||
|
# Count installed modules
|
||||||
|
local module_count=0
|
||||||
|
if [ -d "$DATA_DIR/modules" ]; then
|
||||||
|
module_count=$(ls -d "$DATA_DIR/modules"/MMM-* "$DATA_DIR/modules"/mm-* 2>/dev/null | wc -l)
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"pid": ${pid:-0},
|
||||||
|
"lxc_state": "$lxc_state",
|
||||||
|
"port": $port,
|
||||||
|
"web_url": "$web_url",
|
||||||
|
"module_count": $module_count,
|
||||||
|
"data_path": "$DATA_DIR"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get main configuration
|
||||||
|
get_config() {
|
||||||
|
local enabled=$(uci -q get magicmirror2.main.enabled || echo "0")
|
||||||
|
local port=$(uci -q get magicmirror2.main.port || echo "8082")
|
||||||
|
local address=$(uci -q get magicmirror2.main.address || echo "0.0.0.0")
|
||||||
|
local data_path=$(uci -q get magicmirror2.main.data_path || echo "/srv/magicmirror2")
|
||||||
|
local memory_limit=$(uci -q get magicmirror2.main.memory_limit || echo "512M")
|
||||||
|
local language=$(uci -q get magicmirror2.main.language || echo "en")
|
||||||
|
local timezone=$(uci -q get magicmirror2.main.timezone || echo "Europe/Paris")
|
||||||
|
local units=$(uci -q get magicmirror2.main.units || echo "metric")
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"port": $port,
|
||||||
|
"address": "$address",
|
||||||
|
"data_path": "$data_path",
|
||||||
|
"memory_limit": "$memory_limit",
|
||||||
|
"language": "$language",
|
||||||
|
"timezone": "$timezone",
|
||||||
|
"units": "$units"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get display configuration
|
||||||
|
get_display_config() {
|
||||||
|
local width=$(uci -q get magicmirror2.display.width || echo "1920")
|
||||||
|
local height=$(uci -q get magicmirror2.display.height || echo "1080")
|
||||||
|
local zoom=$(uci -q get magicmirror2.display.zoom || echo "1.0")
|
||||||
|
local brightness=$(uci -q get magicmirror2.display.brightness || echo "100")
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"width": $width,
|
||||||
|
"height": $height,
|
||||||
|
"zoom": $zoom,
|
||||||
|
"brightness": $brightness
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get weather configuration
|
||||||
|
get_weather_config() {
|
||||||
|
local enabled=$(uci -q get magicmirror2.weather.enabled || echo "0")
|
||||||
|
local provider=$(uci -q get magicmirror2.weather.provider || echo "openweathermap")
|
||||||
|
local api_key=$(uci -q get magicmirror2.weather.api_key || echo "")
|
||||||
|
local location=$(uci -q get magicmirror2.weather.location || echo "")
|
||||||
|
local location_id=$(uci -q get magicmirror2.weather.location_id || echo "")
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"provider": "$provider",
|
||||||
|
"api_key": "$api_key",
|
||||||
|
"location": "$location",
|
||||||
|
"location_id": "$location_id"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get module configuration
|
||||||
|
get_modules_config() {
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"clock": {
|
||||||
|
"enabled": $([ "$(uci -q get magicmirror2.clock.enabled || echo 1)" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"display_seconds": $([ "$(uci -q get magicmirror2.clock.display_seconds || echo 1)" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"show_date": $([ "$(uci -q get magicmirror2.clock.show_date || echo 1)" = "1" ] && echo "true" || echo "false")
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"enabled": $([ "$(uci -q get magicmirror2.calendar.enabled || echo 0)" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"max_entries": $(uci -q get magicmirror2.calendar.max_entries || echo 10)
|
||||||
|
},
|
||||||
|
"newsfeed": {
|
||||||
|
"enabled": $([ "$(uci -q get magicmirror2.newsfeed.enabled || echo 0)" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"max_news_items": $(uci -q get magicmirror2.newsfeed.max_news_items || echo 5)
|
||||||
|
},
|
||||||
|
"compliments": {
|
||||||
|
"enabled": $([ "$(uci -q get magicmirror2.compliments.enabled || echo 1)" = "1" ] && echo "true" || echo "false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# List installed modules
|
||||||
|
get_installed_modules() {
|
||||||
|
local modules_dir="$DATA_DIR/modules"
|
||||||
|
|
||||||
|
echo '{"modules":['
|
||||||
|
|
||||||
|
local first=1
|
||||||
|
if [ -d "$modules_dir" ]; then
|
||||||
|
# List MMM-* modules
|
||||||
|
for module_dir in "$modules_dir"/MMM-*; do
|
||||||
|
[ -d "$module_dir" ] || continue
|
||||||
|
[ -f "$module_dir/package.json" ] || continue
|
||||||
|
local name=$(basename "$module_dir")
|
||||||
|
local version=$(jsonfilter -i "$module_dir/package.json" -e '@.version' 2>/dev/null || echo "unknown")
|
||||||
|
local desc=$(jsonfilter -i "$module_dir/package.json" -e '@.description' 2>/dev/null | head -c 100 | sed 's/"/\\"/g')
|
||||||
|
local author=$(jsonfilter -i "$module_dir/package.json" -e '@.author' 2>/dev/null | sed 's/"/\\"/g')
|
||||||
|
local has_config="false"
|
||||||
|
[ -f "$DATA_DIR/config/${name}.json" ] && has_config="true"
|
||||||
|
|
||||||
|
[ "$first" = "1" ] || echo ","
|
||||||
|
first=0
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"name": "$name",
|
||||||
|
"version": "$version",
|
||||||
|
"description": "$desc",
|
||||||
|
"author": "$author",
|
||||||
|
"has_config": $has_config,
|
||||||
|
"path": "$module_dir"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
# List mm-* modules
|
||||||
|
for module_dir in "$modules_dir"/mm-*; do
|
||||||
|
[ -d "$module_dir" ] || continue
|
||||||
|
[ -f "$module_dir/package.json" ] || continue
|
||||||
|
local name=$(basename "$module_dir")
|
||||||
|
local version=$(jsonfilter -i "$module_dir/package.json" -e '@.version' 2>/dev/null || echo "unknown")
|
||||||
|
local desc=$(jsonfilter -i "$module_dir/package.json" -e '@.description' 2>/dev/null | head -c 100 | sed 's/"/\\"/g')
|
||||||
|
local author=$(jsonfilter -i "$module_dir/package.json" -e '@.author' 2>/dev/null | sed 's/"/\\"/g')
|
||||||
|
local has_config="false"
|
||||||
|
[ -f "$DATA_DIR/config/${name}.json" ] && has_config="true"
|
||||||
|
|
||||||
|
[ "$first" = "1" ] || echo ","
|
||||||
|
first=0
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"name": "$name",
|
||||||
|
"version": "$version",
|
||||||
|
"description": "$desc",
|
||||||
|
"author": "$author",
|
||||||
|
"has_config": $has_config,
|
||||||
|
"path": "$module_dir"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ']}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Service control
|
||||||
|
service_start() {
|
||||||
|
/etc/init.d/magicmirror2 start >/dev/null 2>&1
|
||||||
|
sleep 3
|
||||||
|
get_status
|
||||||
|
}
|
||||||
|
|
||||||
|
service_stop() {
|
||||||
|
/etc/init.d/magicmirror2 stop >/dev/null 2>&1
|
||||||
|
sleep 1
|
||||||
|
get_status
|
||||||
|
}
|
||||||
|
|
||||||
|
service_restart() {
|
||||||
|
/etc/init.d/magicmirror2 restart >/dev/null 2>&1
|
||||||
|
sleep 3
|
||||||
|
get_status
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install module
|
||||||
|
install_module() {
|
||||||
|
local module_name="$1"
|
||||||
|
|
||||||
|
if [ -z "$module_name" ]; then
|
||||||
|
echo '{"success":false,"message":"Module name required"}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local result=$(/usr/sbin/mm2ctl module install "$module_name" 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
echo '{"success":true,"message":"Module installed successfully"}'
|
||||||
|
else
|
||||||
|
local escaped=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
|
||||||
|
echo "{\"success\":false,\"message\":\"$escaped\"}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove module
|
||||||
|
remove_module() {
|
||||||
|
local module_name="$1"
|
||||||
|
|
||||||
|
if [ -z "$module_name" ]; then
|
||||||
|
echo '{"success":false,"message":"Module name required"}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local result=$(/usr/sbin/mm2ctl module remove "$module_name" 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
echo '{"success":true,"message":"Module removed successfully"}'
|
||||||
|
else
|
||||||
|
local escaped=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
|
||||||
|
echo "{\"success\":false,\"message\":\"$escaped\"}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update module(s)
|
||||||
|
update_modules() {
|
||||||
|
local module_name="$1"
|
||||||
|
|
||||||
|
local result=$(/usr/sbin/mm2ctl module update $module_name 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
echo '{"success":true,"message":"Modules updated successfully"}'
|
||||||
|
else
|
||||||
|
local escaped=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
|
||||||
|
echo "{\"success\":false,\"message\":\"$escaped\"}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Regenerate config
|
||||||
|
regenerate_config() {
|
||||||
|
/usr/sbin/mm2ctl config >/dev/null 2>&1
|
||||||
|
echo '{"success":true,"message":"Configuration regenerated"}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set configuration
|
||||||
|
set_config() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
|
||||||
|
if [ -z "$key" ] || [ -z "$value" ]; then
|
||||||
|
echo '{"success":false,"message":"Key and value required"}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Handle boolean conversion
|
||||||
|
case "$value" in
|
||||||
|
true) value="1" ;;
|
||||||
|
false) value="0" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Determine section based on key
|
||||||
|
local section="main"
|
||||||
|
case "$key" in
|
||||||
|
width|height|zoom|brightness)
|
||||||
|
section="display"
|
||||||
|
;;
|
||||||
|
provider|api_key|location|location_id)
|
||||||
|
section="weather"
|
||||||
|
;;
|
||||||
|
display_seconds|show_date|show_week)
|
||||||
|
section="clock"
|
||||||
|
;;
|
||||||
|
max_entries|fetch_interval)
|
||||||
|
section="calendar"
|
||||||
|
;;
|
||||||
|
max_news_items|show_description|show_source_title)
|
||||||
|
section="newsfeed"
|
||||||
|
;;
|
||||||
|
update_interval)
|
||||||
|
section="compliments"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
uci set "magicmirror2.$section.$key=$value"
|
||||||
|
uci commit magicmirror2
|
||||||
|
|
||||||
|
echo '{"success":true}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get web URL for iframe
|
||||||
|
get_web_url() {
|
||||||
|
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||||
|
local port=$(uci -q get magicmirror2.main.port || echo "8082")
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"web_url": "http://$router_ip:$port",
|
||||||
|
"port": $port
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# RPCD list method
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"get_status": {},
|
||||||
|
"get_config": {},
|
||||||
|
"get_display_config": {},
|
||||||
|
"get_weather_config": {},
|
||||||
|
"get_modules_config": {},
|
||||||
|
"get_installed_modules": {},
|
||||||
|
"get_web_url": {},
|
||||||
|
"service_start": {},
|
||||||
|
"service_stop": {},
|
||||||
|
"service_restart": {},
|
||||||
|
"install_module": {"name": "string"},
|
||||||
|
"remove_module": {"name": "string"},
|
||||||
|
"update_modules": {"name": "string"},
|
||||||
|
"regenerate_config": {},
|
||||||
|
"set_config": {"key": "string", "value": "string"}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
get_status)
|
||||||
|
get_status
|
||||||
|
;;
|
||||||
|
get_config)
|
||||||
|
get_config
|
||||||
|
;;
|
||||||
|
get_display_config)
|
||||||
|
get_display_config
|
||||||
|
;;
|
||||||
|
get_weather_config)
|
||||||
|
get_weather_config
|
||||||
|
;;
|
||||||
|
get_modules_config)
|
||||||
|
get_modules_config
|
||||||
|
;;
|
||||||
|
get_installed_modules)
|
||||||
|
get_installed_modules
|
||||||
|
;;
|
||||||
|
get_web_url)
|
||||||
|
get_web_url
|
||||||
|
;;
|
||||||
|
service_start)
|
||||||
|
service_start
|
||||||
|
;;
|
||||||
|
service_stop)
|
||||||
|
service_stop
|
||||||
|
;;
|
||||||
|
service_restart)
|
||||||
|
service_restart
|
||||||
|
;;
|
||||||
|
install_module)
|
||||||
|
read -r input
|
||||||
|
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||||
|
install_module "$name"
|
||||||
|
;;
|
||||||
|
remove_module)
|
||||||
|
read -r input
|
||||||
|
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||||
|
remove_module "$name"
|
||||||
|
;;
|
||||||
|
update_modules)
|
||||||
|
read -r input
|
||||||
|
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||||
|
update_modules "$name"
|
||||||
|
;;
|
||||||
|
regenerate_config)
|
||||||
|
regenerate_config
|
||||||
|
;;
|
||||||
|
set_config)
|
||||||
|
read -r input
|
||||||
|
key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null)
|
||||||
|
value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null)
|
||||||
|
set_config "$key" "$value"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"error":"Unknown method"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"error":"Unknown command"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"admin/secubox/services/magicmirror2": {
|
||||||
|
"title": "MagicMirror2",
|
||||||
|
"order": 60,
|
||||||
|
"action": {
|
||||||
|
"type": "firstchild"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": ["luci-app-magicmirror2"],
|
||||||
|
"uci": {"magicmirror2": true}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/secubox/services/magicmirror2/dashboard": {
|
||||||
|
"title": "Dashboard",
|
||||||
|
"order": 10,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "magicmirror2/dashboard"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/secubox/services/magicmirror2/webui": {
|
||||||
|
"title": "Display",
|
||||||
|
"order": 15,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "magicmirror2/webui"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/secubox/services/magicmirror2/modules": {
|
||||||
|
"title": "Modules",
|
||||||
|
"order": 20,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "magicmirror2/modules"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/secubox/services/magicmirror2/settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"order": 30,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "magicmirror2/settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"luci-app-magicmirror2": {
|
||||||
|
"description": "Grant access to MagicMirror2 dashboard",
|
||||||
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.magicmirror2": [
|
||||||
|
"get_status",
|
||||||
|
"get_config",
|
||||||
|
"get_display_config",
|
||||||
|
"get_weather_config",
|
||||||
|
"get_modules_config",
|
||||||
|
"get_installed_modules",
|
||||||
|
"get_web_url"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uci": ["magicmirror2"]
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.magicmirror2": [
|
||||||
|
"service_start",
|
||||||
|
"service_stop",
|
||||||
|
"service_restart",
|
||||||
|
"install_module",
|
||||||
|
"remove_module",
|
||||||
|
"update_modules",
|
||||||
|
"regenerate_config",
|
||||||
|
"set_config"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uci": ["magicmirror2"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
package/secubox/luci-app-mmpm/Makefile
Normal file
57
package/secubox/luci-app-mmpm/Makefile
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
#
|
||||||
|
# Copyright (C) 2026 CyberMind.fr
|
||||||
|
#
|
||||||
|
# LuCI MMPM - MagicMirror Package Manager Interface
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=luci-app-mmpm
|
||||||
|
PKG_VERSION:=0.2.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
PKG_ARCH:=all
|
||||||
|
|
||||||
|
PKG_LICENSE:=MIT
|
||||||
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI MMPM Dashboard
|
||||||
|
LUCI_DESCRIPTION:=Web interface for MMPM - MagicMirror Package Manager
|
||||||
|
LUCI_DEPENDS:=+luci-base +luci-app-secubox +secubox-app-mmpm
|
||||||
|
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/install
|
||||||
|
# RPCD backend
|
||||||
|
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||||
|
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.mmpm $(1)/usr/libexec/rpcd/
|
||||||
|
|
||||||
|
# ACL permissions
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
|
||||||
|
|
||||||
|
# LuCI menu
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
|
||||||
|
|
||||||
|
# JavaScript resources
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/mmpm
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/mmpm/*.js $(1)/www/luci-static/resources/mmpm/ 2>/dev/null || true
|
||||||
|
|
||||||
|
# JavaScript views
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/mmpm
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/mmpm/*.js $(1)/www/luci-static/resources/view/mmpm/
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/postinst
|
||||||
|
#!/bin/sh
|
||||||
|
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||||
|
/etc/init.d/rpcd restart
|
||||||
|
rm -rf /tmp/luci-modulecache /tmp/luci-indexcache 2>/dev/null
|
||||||
|
echo "MMPM Dashboard installed."
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,luci-app-mmpm))
|
||||||
@ -0,0 +1,150 @@
|
|||||||
|
'use strict';
|
||||||
|
'require baseclass';
|
||||||
|
'require rpc';
|
||||||
|
|
||||||
|
var callGetStatus = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'get_status',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetConfig = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'get_config',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetWebUrl = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'get_web_url',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callInstallMmpm = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'install_mmpm',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callUpdateMmpm = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'update_mmpm',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callServiceStart = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'service_start',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callServiceStop = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'service_stop',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callServiceRestart = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'service_restart',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callSearchModules = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'search_modules',
|
||||||
|
params: ['query'],
|
||||||
|
expect: { modules: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callListModules = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'list_modules',
|
||||||
|
expect: { modules: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callInstallModule = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'install_module',
|
||||||
|
params: ['name'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callRemoveModule = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'remove_module',
|
||||||
|
params: ['name'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callUpgradeModules = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'upgrade_modules',
|
||||||
|
params: ['name'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callSetConfig = rpc.declare({
|
||||||
|
object: 'luci.mmpm',
|
||||||
|
method: 'set_config',
|
||||||
|
params: ['key', 'value'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
return baseclass.extend({
|
||||||
|
getStatus: function() {
|
||||||
|
return callGetStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
getConfig: function() {
|
||||||
|
return callGetConfig();
|
||||||
|
},
|
||||||
|
|
||||||
|
getWebUrl: function() {
|
||||||
|
return callGetWebUrl();
|
||||||
|
},
|
||||||
|
|
||||||
|
installMmpm: function() {
|
||||||
|
return callInstallMmpm();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateMmpm: function() {
|
||||||
|
return callUpdateMmpm();
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceStart: function() {
|
||||||
|
return callServiceStart();
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceStop: function() {
|
||||||
|
return callServiceStop();
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceRestart: function() {
|
||||||
|
return callServiceRestart();
|
||||||
|
},
|
||||||
|
|
||||||
|
searchModules: function(query) {
|
||||||
|
return callSearchModules(query);
|
||||||
|
},
|
||||||
|
|
||||||
|
listModules: function() {
|
||||||
|
return callListModules();
|
||||||
|
},
|
||||||
|
|
||||||
|
installModule: function(name) {
|
||||||
|
return callInstallModule(name);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeModule: function(name) {
|
||||||
|
return callRemoveModule(name);
|
||||||
|
},
|
||||||
|
|
||||||
|
upgradeModules: function(name) {
|
||||||
|
return callUpgradeModules(name || '');
|
||||||
|
},
|
||||||
|
|
||||||
|
setConfig: function(key, value) {
|
||||||
|
return callSetConfig(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,199 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require mmpm.api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
title: _('MMPM Dashboard'),
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return api.getStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(status) {
|
||||||
|
var self = this;
|
||||||
|
status = status || {};
|
||||||
|
|
||||||
|
var wrapper = E('div', { 'class': 'cbi-map', 'style': 'background: #0d0d12; min-height: 100vh; padding: 20px;' });
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
wrapper.appendChild(E('style', {}, [
|
||||||
|
':root { --mmpm-primary: #f39c12; --mmpm-secondary: #e67e22; }',
|
||||||
|
'.mmpm-header { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; padding: 20px; background: linear-gradient(135deg, rgba(243,156,18,0.1), rgba(230,126,34,0.05)); border-radius: 16px; border: 1px solid rgba(243,156,18,0.2); }',
|
||||||
|
'.mmpm-logo { font-size: 48px; }',
|
||||||
|
'.mmpm-title { font-size: 28px; font-weight: 700; color: #fff; }',
|
||||||
|
'.mmpm-subtitle { color: #a0a0b0; font-size: 14px; }',
|
||||||
|
'.mmpm-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }',
|
||||||
|
'.mmpm-card { background: #141419; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 20px; }',
|
||||||
|
'.mmpm-stat { text-align: center; }',
|
||||||
|
'.mmpm-stat-value { font-size: 32px; font-weight: 700; color: #f39c12; }',
|
||||||
|
'.mmpm-stat-label { font-size: 12px; color: #a0a0b0; margin-top: 4px; }',
|
||||||
|
'.mmpm-status-badge { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; }',
|
||||||
|
'.mmpm-status-running { background: rgba(34,197,94,0.15); color: #22c55e; }',
|
||||||
|
'.mmpm-status-stopped { background: rgba(239,68,68,0.15); color: #ef4444; }',
|
||||||
|
'.mmpm-status-notinstalled { background: rgba(161,161,170,0.15); color: #a1a1aa; }',
|
||||||
|
'.mmpm-btn { padding: 12px 24px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; display: inline-flex; align-items: center; gap: 8px; }',
|
||||||
|
'.mmpm-btn-primary { background: linear-gradient(135deg, #f39c12, #e67e22); color: white; }',
|
||||||
|
'.mmpm-btn-primary:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(243,156,18,0.3); }',
|
||||||
|
'.mmpm-btn-secondary { background: rgba(255,255,255,0.1); color: white; }',
|
||||||
|
'.mmpm-btn-success { background: linear-gradient(135deg, #22c55e, #16a34a); color: white; }',
|
||||||
|
'.mmpm-btn-danger { background: linear-gradient(135deg, #ef4444, #dc2626); color: white; }',
|
||||||
|
'.mmpm-actions { display: flex; gap: 12px; flex-wrap: wrap; }',
|
||||||
|
'.mmpm-section { margin-bottom: 24px; }',
|
||||||
|
'.mmpm-section-title { font-size: 18px; font-weight: 600; color: #fff; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }'
|
||||||
|
].join('')));
|
||||||
|
|
||||||
|
// Header
|
||||||
|
wrapper.appendChild(E('div', { 'class': 'mmpm-header' }, [
|
||||||
|
E('div', { 'class': 'mmpm-logo' }, '📦'),
|
||||||
|
E('div', {}, [
|
||||||
|
E('div', { 'class': 'mmpm-title' }, 'MMPM'),
|
||||||
|
E('div', { 'class': 'mmpm-subtitle' }, _('MagicMirror Package Manager'))
|
||||||
|
])
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Status badges
|
||||||
|
var mm2Badge = status.mm2_running
|
||||||
|
? E('span', { 'class': 'mmpm-status-badge mmpm-status-running' }, ['● ', _('MM2 Running')])
|
||||||
|
: E('span', { 'class': 'mmpm-status-badge mmpm-status-stopped' }, ['○ ', _('MM2 Stopped')]);
|
||||||
|
|
||||||
|
var mmpmBadge;
|
||||||
|
if (!status.installed) {
|
||||||
|
mmpmBadge = E('span', { 'class': 'mmpm-status-badge mmpm-status-notinstalled' }, ['○ ', _('Not Installed')]);
|
||||||
|
} else if (status.gui_running) {
|
||||||
|
mmpmBadge = E('span', { 'class': 'mmpm-status-badge mmpm-status-running' }, ['● ', _('GUI Running')]);
|
||||||
|
} else {
|
||||||
|
mmpmBadge = E('span', { 'class': 'mmpm-status-badge mmpm-status-stopped' }, ['○ ', _('GUI Stopped')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats grid
|
||||||
|
wrapper.appendChild(E('div', { 'class': 'mmpm-grid' }, [
|
||||||
|
E('div', { 'class': 'mmpm-card' }, [
|
||||||
|
E('div', { 'class': 'mmpm-stat' }, [
|
||||||
|
mm2Badge,
|
||||||
|
E('div', { 'class': 'mmpm-stat-label', 'style': 'margin-top: 8px;' }, _('MagicMirror2'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'mmpm-card' }, [
|
||||||
|
E('div', { 'class': 'mmpm-stat' }, [
|
||||||
|
mmpmBadge,
|
||||||
|
E('div', { 'class': 'mmpm-stat-label', 'style': 'margin-top: 8px;' }, _('MMPM Status'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'mmpm-card' }, [
|
||||||
|
E('div', { 'class': 'mmpm-stat' }, [
|
||||||
|
E('div', { 'class': 'mmpm-stat-value' }, status.version || '-'),
|
||||||
|
E('div', { 'class': 'mmpm-stat-label' }, _('Version'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'mmpm-card' }, [
|
||||||
|
E('div', { 'class': 'mmpm-stat' }, [
|
||||||
|
E('div', { 'class': 'mmpm-stat-value' }, ':' + (status.port || 7891)),
|
||||||
|
E('div', { 'class': 'mmpm-stat-label' }, _('GUI Port'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Actions section
|
||||||
|
var actionsSection = E('div', { 'class': 'mmpm-section' });
|
||||||
|
actionsSection.appendChild(E('div', { 'class': 'mmpm-section-title' }, ['⚡ ', _('Actions')]));
|
||||||
|
|
||||||
|
var actionsDiv = E('div', { 'class': 'mmpm-actions' });
|
||||||
|
|
||||||
|
if (!status.installed) {
|
||||||
|
// Install button
|
||||||
|
actionsDiv.appendChild(E('button', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-primary',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal(_('Installing MMPM'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Installing MMPM in MagicMirror2 container...'))
|
||||||
|
]);
|
||||||
|
api.installMmpm().then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('MMPM installed successfully')), 'success');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, res.message || _('Installation failed')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ['📥 ', _('Install MMPM')]));
|
||||||
|
} else {
|
||||||
|
// Start/Stop buttons
|
||||||
|
if (status.gui_running) {
|
||||||
|
actionsDiv.appendChild(E('button', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-danger',
|
||||||
|
'click': function() {
|
||||||
|
api.serviceStop().then(function() {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ['⏹ ', _('Stop GUI')]));
|
||||||
|
} else {
|
||||||
|
actionsDiv.appendChild(E('button', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-success',
|
||||||
|
'click': function() {
|
||||||
|
api.serviceStart().then(function() {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ['▶ ', _('Start GUI')]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update button
|
||||||
|
actionsDiv.appendChild(E('button', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-secondary',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal(_('Updating MMPM'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Updating MMPM...'))
|
||||||
|
]);
|
||||||
|
api.updateMmpm().then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('MMPM updated')), 'success');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, res.message), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ['🔄 ', _('Update MMPM')]));
|
||||||
|
|
||||||
|
// Open GUI button
|
||||||
|
if (status.gui_running && status.web_url) {
|
||||||
|
actionsDiv.appendChild(E('a', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-primary',
|
||||||
|
'href': status.web_url,
|
||||||
|
'target': '_blank'
|
||||||
|
}, ['🌐 ', _('Open Web GUI')]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actionsSection.appendChild(actionsDiv);
|
||||||
|
wrapper.appendChild(actionsSection);
|
||||||
|
|
||||||
|
// Quick links
|
||||||
|
if (status.installed) {
|
||||||
|
var linksSection = E('div', { 'class': 'mmpm-section' });
|
||||||
|
linksSection.appendChild(E('div', { 'class': 'mmpm-section-title' }, ['🔗 ', _('Quick Links')]));
|
||||||
|
linksSection.appendChild(E('div', { 'class': 'mmpm-actions' }, [
|
||||||
|
E('a', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-secondary',
|
||||||
|
'href': L.url('admin', 'secubox', 'services', 'mmpm', 'modules')
|
||||||
|
}, ['🧩 ', _('Manage Modules')]),
|
||||||
|
E('a', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-secondary',
|
||||||
|
'href': L.url('admin', 'secubox', 'services', 'mmpm', 'settings')
|
||||||
|
}, ['⚙️ ', _('Settings')]),
|
||||||
|
E('a', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-secondary',
|
||||||
|
'href': L.url('admin', 'secubox', 'services', 'magicmirror2', 'dashboard')
|
||||||
|
}, ['🪞 ', _('MagicMirror2')])
|
||||||
|
]));
|
||||||
|
wrapper.appendChild(linksSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require mmpm.api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
title: _('MMPM Modules'),
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
api.getStatus(),
|
||||||
|
api.listModules()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var status = data[0] || {};
|
||||||
|
var modulesData = data[1] || {};
|
||||||
|
var modules = modulesData.modules || [];
|
||||||
|
|
||||||
|
var wrapper = E('div', { 'class': 'cbi-map', 'style': 'background: #0d0d12; min-height: 100vh; padding: 20px;' });
|
||||||
|
|
||||||
|
wrapper.appendChild(E('style', {}, [
|
||||||
|
'.mmpm-search { display: flex; gap: 12px; margin-bottom: 24px; }',
|
||||||
|
'.mmpm-search input { flex: 1; padding: 12px 16px; background: #141419; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: white; font-size: 14px; }',
|
||||||
|
'.mmpm-search input:focus { outline: none; border-color: #f39c12; }',
|
||||||
|
'.mmpm-btn { padding: 12px 24px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }',
|
||||||
|
'.mmpm-btn-primary { background: linear-gradient(135deg, #f39c12, #e67e22); color: white; }',
|
||||||
|
'.mmpm-modules-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }',
|
||||||
|
'.mmpm-module-card { background: #141419; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 16px; }',
|
||||||
|
'.mmpm-module-name { font-size: 16px; font-weight: 600; color: #f39c12; margin-bottom: 8px; }',
|
||||||
|
'.mmpm-module-desc { font-size: 13px; color: #a0a0b0; margin-bottom: 12px; line-height: 1.4; }',
|
||||||
|
'.mmpm-module-actions { display: flex; gap: 8px; }',
|
||||||
|
'.mmpm-btn-sm { padding: 8px 16px; font-size: 12px; }',
|
||||||
|
'.mmpm-btn-danger { background: rgba(239,68,68,0.2); color: #ef4444; }',
|
||||||
|
'.mmpm-btn-secondary { background: rgba(255,255,255,0.1); color: white; }',
|
||||||
|
'.mmpm-section-title { font-size: 20px; font-weight: 600; color: #fff; margin-bottom: 16px; }',
|
||||||
|
'.mmpm-empty { text-align: center; padding: 40px; color: #a0a0b0; }'
|
||||||
|
].join('')));
|
||||||
|
|
||||||
|
// Search section
|
||||||
|
var searchInput = E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'placeholder': _('Search modules (e.g., weather, calendar, spotify)...'),
|
||||||
|
'id': 'mmpm-search-input'
|
||||||
|
});
|
||||||
|
|
||||||
|
var searchBtn = E('button', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-primary',
|
||||||
|
'click': function() {
|
||||||
|
var query = searchInput.value.trim();
|
||||||
|
if (!query) return;
|
||||||
|
|
||||||
|
ui.showModal(_('Searching'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Searching modules...'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
api.searchModules(query).then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
// Display results...
|
||||||
|
var results = res.modules || [];
|
||||||
|
alert(_('Found %d modules').format(results.length));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, _('Search'));
|
||||||
|
|
||||||
|
wrapper.appendChild(E('div', { 'class': 'mmpm-search' }, [searchInput, searchBtn]));
|
||||||
|
|
||||||
|
// Installed modules
|
||||||
|
wrapper.appendChild(E('div', { 'class': 'mmpm-section-title' }, _('Installed Modules')));
|
||||||
|
|
||||||
|
if (modules.length === 0) {
|
||||||
|
wrapper.appendChild(E('div', { 'class': 'mmpm-empty' }, [
|
||||||
|
E('p', {}, _('No modules installed yet.')),
|
||||||
|
E('p', {}, _('Use the search above or MMPM Web GUI to install modules.'))
|
||||||
|
]));
|
||||||
|
} else {
|
||||||
|
var grid = E('div', { 'class': 'mmpm-modules-grid' });
|
||||||
|
|
||||||
|
modules.forEach(function(mod) {
|
||||||
|
var card = E('div', { 'class': 'mmpm-module-card' }, [
|
||||||
|
E('div', { 'class': 'mmpm-module-name' }, mod.name || mod.title || 'Unknown'),
|
||||||
|
E('div', { 'class': 'mmpm-module-desc' }, mod.description || ''),
|
||||||
|
E('div', { 'class': 'mmpm-module-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-sm mmpm-btn-secondary',
|
||||||
|
'click': function() {
|
||||||
|
api.upgradeModules(mod.name).then(function(res) {
|
||||||
|
ui.addNotification(null, E('p', {}, res.message), res.success ? 'success' : 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, _('Update')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-sm mmpm-btn-danger',
|
||||||
|
'click': function() {
|
||||||
|
if (confirm(_('Remove module %s?').format(mod.name))) {
|
||||||
|
api.removeModule(mod.name).then(function(res) {
|
||||||
|
if (res.success) window.location.reload();
|
||||||
|
else ui.addNotification(null, E('p', {}, res.message), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, _('Remove'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.appendChild(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require uci';
|
||||||
|
'require form';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
title: _('MMPM Settings'),
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return uci.load('mmpm');
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var m, s, o;
|
||||||
|
|
||||||
|
m = new form.Map('mmpm', _('MMPM Settings'), _('Configure MMPM - MagicMirror Package Manager'));
|
||||||
|
|
||||||
|
s = m.section(form.NamedSection, 'main', 'mmpm', _('General Settings'));
|
||||||
|
s.anonymous = true;
|
||||||
|
s.addremove = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'enabled', _('Enable GUI Service'));
|
||||||
|
o.rmempty = false;
|
||||||
|
o.default = '0';
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'port', _('GUI Port'));
|
||||||
|
o.datatype = 'port';
|
||||||
|
o.default = '7891';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'address', _('Listen Address'));
|
||||||
|
o.default = '0.0.0.0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
return m.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require mmpm.api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
title: _('MMPM Web GUI'),
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return api.getStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(status) {
|
||||||
|
status = status || {};
|
||||||
|
|
||||||
|
var wrapper = E('div', { 'style': 'background: #0d0d12; min-height: 100vh;' });
|
||||||
|
|
||||||
|
wrapper.appendChild(E('style', {}, [
|
||||||
|
'.mmpm-webui-toolbar { display: flex; align-items: center; justify-content: space-between; padding: 12px 20px; background: #141419; border-bottom: 1px solid rgba(255,255,255,0.08); }',
|
||||||
|
'.mmpm-webui-title { font-size: 16px; font-weight: 600; color: #f39c12; display: flex; align-items: center; gap: 8px; }',
|
||||||
|
'.mmpm-webui-actions { display: flex; gap: 8px; }',
|
||||||
|
'.mmpm-btn { padding: 8px 16px; border: none; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; }',
|
||||||
|
'.mmpm-btn-secondary { background: rgba(255,255,255,0.1); color: white; }',
|
||||||
|
'.mmpm-webui-frame { width: 100%; height: calc(100vh - 120px); border: none; background: #1a1a1f; }',
|
||||||
|
'.mmpm-webui-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; height: calc(100vh - 120px); color: #a0a0b0; text-align: center; padding: 40px; }',
|
||||||
|
'.mmpm-webui-placeholder h3 { color: #fff; margin-bottom: 16px; }',
|
||||||
|
'.mmpm-btn-primary { background: linear-gradient(135deg, #f39c12, #e67e22); color: white; }'
|
||||||
|
].join('')));
|
||||||
|
|
||||||
|
// Toolbar
|
||||||
|
var toolbar = E('div', { 'class': 'mmpm-webui-toolbar' }, [
|
||||||
|
E('div', { 'class': 'mmpm-webui-title' }, ['📦 ', 'MMPM Web GUI']),
|
||||||
|
E('div', { 'class': 'mmpm-webui-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-secondary',
|
||||||
|
'click': function() {
|
||||||
|
var iframe = document.getElementById('mmpm-iframe');
|
||||||
|
if (iframe) iframe.src = iframe.src;
|
||||||
|
}
|
||||||
|
}, ['🔄 ', _('Refresh')])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (status.web_url) {
|
||||||
|
toolbar.lastChild.appendChild(E('a', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-secondary',
|
||||||
|
'href': status.web_url,
|
||||||
|
'target': '_blank'
|
||||||
|
}, ['↗ ', _('Open in New Tab')]));
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.appendChild(toolbar);
|
||||||
|
|
||||||
|
// Content
|
||||||
|
if (!status.installed) {
|
||||||
|
wrapper.appendChild(E('div', { 'class': 'mmpm-webui-placeholder' }, [
|
||||||
|
E('h3', {}, _('MMPM Not Installed')),
|
||||||
|
E('p', {}, _('Install MMPM from the Dashboard to use the Web GUI.')),
|
||||||
|
E('a', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-primary',
|
||||||
|
'href': L.url('admin', 'secubox', 'services', 'mmpm', 'dashboard'),
|
||||||
|
'style': 'margin-top: 16px;'
|
||||||
|
}, _('Go to Dashboard'))
|
||||||
|
]));
|
||||||
|
} else if (!status.gui_running) {
|
||||||
|
wrapper.appendChild(E('div', { 'class': 'mmpm-webui-placeholder' }, [
|
||||||
|
E('h3', {}, _('MMPM GUI Not Running')),
|
||||||
|
E('p', {}, _('Start the MMPM GUI service to access the web interface.')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'mmpm-btn mmpm-btn-primary',
|
||||||
|
'style': 'margin-top: 16px;',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal(_('Starting'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Starting MMPM GUI...'))
|
||||||
|
]);
|
||||||
|
api.serviceStart().then(function() {
|
||||||
|
ui.hideModal();
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, _('Start GUI'))
|
||||||
|
]));
|
||||||
|
} else {
|
||||||
|
wrapper.appendChild(E('iframe', {
|
||||||
|
'id': 'mmpm-iframe',
|
||||||
|
'class': 'mmpm-webui-frame',
|
||||||
|
'src': status.web_url
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
});
|
||||||
328
package/secubox/luci-app-mmpm/root/usr/libexec/rpcd/luci.mmpm
Normal file
328
package/secubox/luci-app-mmpm/root/usr/libexec/rpcd/luci.mmpm
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# RPCD backend for MMPM LuCI interface
|
||||||
|
# Copyright (C) 2026 CyberMind.fr (SecuBox)
|
||||||
|
#
|
||||||
|
|
||||||
|
. /lib/functions.sh
|
||||||
|
|
||||||
|
LXC_NAME="magicmirror2"
|
||||||
|
|
||||||
|
# Get MMPM status
|
||||||
|
get_status() {
|
||||||
|
local running=0
|
||||||
|
local installed=0
|
||||||
|
local gui_running=0
|
||||||
|
local version=""
|
||||||
|
local web_url=""
|
||||||
|
|
||||||
|
# Check if MM2 container is running
|
||||||
|
local mm2_running=0
|
||||||
|
if lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
mm2_running=1
|
||||||
|
|
||||||
|
# Check if MMPM is installed
|
||||||
|
if lxc-attach -n "$LXC_NAME" -- sh -c "command -v mmpm >/dev/null 2>&1" 2>/dev/null; then
|
||||||
|
installed=1
|
||||||
|
version=$(lxc-attach -n "$LXC_NAME" -- mmpm --version 2>/dev/null || echo "unknown")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check GUI status
|
||||||
|
if pgrep -f "mmpm gui" >/dev/null 2>&1; then
|
||||||
|
gui_running=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local enabled=$(uci -q get mmpm.main.enabled || echo "0")
|
||||||
|
local port=$(uci -q get mmpm.main.port || echo "7891")
|
||||||
|
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||||
|
|
||||||
|
[ "$gui_running" = "1" ] && web_url="http://${router_ip}:${port}"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"mm2_running": $([ "$mm2_running" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"gui_running": $([ "$gui_running" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"version": "$version",
|
||||||
|
"port": $port,
|
||||||
|
"web_url": "$web_url"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get configuration
|
||||||
|
get_config() {
|
||||||
|
local enabled=$(uci -q get mmpm.main.enabled || echo "0")
|
||||||
|
local port=$(uci -q get mmpm.main.port || echo "7891")
|
||||||
|
local address=$(uci -q get mmpm.main.address || echo "0.0.0.0")
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||||
|
"port": $port,
|
||||||
|
"address": "$address"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install MMPM
|
||||||
|
install_mmpm() {
|
||||||
|
local result=$(/usr/sbin/mmpmctl install 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
echo '{"success":true,"message":"MMPM installed successfully"}'
|
||||||
|
else
|
||||||
|
local escaped=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
|
||||||
|
echo "{\"success\":false,\"message\":\"$escaped\"}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update MMPM
|
||||||
|
update_mmpm() {
|
||||||
|
local result=$(/usr/sbin/mmpmctl update 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
echo '{"success":true,"message":"MMPM updated successfully"}'
|
||||||
|
else
|
||||||
|
local escaped=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
|
||||||
|
echo "{\"success\":false,\"message\":\"$escaped\"}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Service control
|
||||||
|
service_start() {
|
||||||
|
/etc/init.d/mmpm start >/dev/null 2>&1
|
||||||
|
sleep 2
|
||||||
|
get_status
|
||||||
|
}
|
||||||
|
|
||||||
|
service_stop() {
|
||||||
|
/etc/init.d/mmpm stop >/dev/null 2>&1
|
||||||
|
sleep 1
|
||||||
|
get_status
|
||||||
|
}
|
||||||
|
|
||||||
|
service_restart() {
|
||||||
|
/etc/init.d/mmpm restart >/dev/null 2>&1
|
||||||
|
sleep 2
|
||||||
|
get_status
|
||||||
|
}
|
||||||
|
|
||||||
|
# Search modules
|
||||||
|
search_modules() {
|
||||||
|
local query="$1"
|
||||||
|
|
||||||
|
if [ -z "$query" ]; then
|
||||||
|
echo '{"modules":[],"error":"Query required"}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
echo '{"modules":[],"error":"MagicMirror2 not running"}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local results=$(lxc-attach -n "$LXC_NAME" -- mmpm search "$query" --json 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$results" ]; then
|
||||||
|
echo "$results"
|
||||||
|
else
|
||||||
|
echo '{"modules":[]}'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# List installed modules
|
||||||
|
list_modules() {
|
||||||
|
if ! lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
echo '{"modules":[],"error":"MagicMirror2 not running"}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local results=$(lxc-attach -n "$LXC_NAME" -- mmpm list --json 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$results" ]; then
|
||||||
|
echo "$results"
|
||||||
|
else
|
||||||
|
echo '{"modules":[]}'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install module
|
||||||
|
install_module() {
|
||||||
|
local name="$1"
|
||||||
|
|
||||||
|
if [ -z "$name" ]; then
|
||||||
|
echo '{"success":false,"message":"Module name required"}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local result=$(/usr/sbin/mmpmctl install-module "$name" 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
echo '{"success":true,"message":"Module installed successfully"}'
|
||||||
|
else
|
||||||
|
local escaped=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
|
||||||
|
echo "{\"success\":false,\"message\":\"$escaped\"}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove module
|
||||||
|
remove_module() {
|
||||||
|
local name="$1"
|
||||||
|
|
||||||
|
if [ -z "$name" ]; then
|
||||||
|
echo '{"success":false,"message":"Module name required"}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local result=$(/usr/sbin/mmpmctl remove "$name" 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
echo '{"success":true,"message":"Module removed successfully"}'
|
||||||
|
else
|
||||||
|
local escaped=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
|
||||||
|
echo "{\"success\":false,\"message\":\"$escaped\"}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upgrade modules
|
||||||
|
upgrade_modules() {
|
||||||
|
local name="$1"
|
||||||
|
|
||||||
|
local result=$(/usr/sbin/mmpmctl upgrade $name 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
echo '{"success":true,"message":"Modules upgraded successfully"}'
|
||||||
|
else
|
||||||
|
local escaped=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
|
||||||
|
echo "{\"success\":false,\"message\":\"$escaped\"}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get web URL for iframe
|
||||||
|
get_web_url() {
|
||||||
|
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||||
|
local port=$(uci -q get mmpm.main.port || echo "7891")
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"web_url": "http://$router_ip:$port",
|
||||||
|
"port": $port
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set configuration
|
||||||
|
set_config() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
|
||||||
|
if [ -z "$key" ] || [ -z "$value" ]; then
|
||||||
|
echo '{"success":false,"message":"Key and value required"}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$value" in
|
||||||
|
true) value="1" ;;
|
||||||
|
false) value="0" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
uci set "mmpm.main.$key=$value"
|
||||||
|
uci commit mmpm
|
||||||
|
|
||||||
|
echo '{"success":true}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# RPCD list method
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"get_status": {},
|
||||||
|
"get_config": {},
|
||||||
|
"get_web_url": {},
|
||||||
|
"install_mmpm": {},
|
||||||
|
"update_mmpm": {},
|
||||||
|
"service_start": {},
|
||||||
|
"service_stop": {},
|
||||||
|
"service_restart": {},
|
||||||
|
"search_modules": {"query": "string"},
|
||||||
|
"list_modules": {},
|
||||||
|
"install_module": {"name": "string"},
|
||||||
|
"remove_module": {"name": "string"},
|
||||||
|
"upgrade_modules": {"name": "string"},
|
||||||
|
"set_config": {"key": "string", "value": "string"}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
get_status)
|
||||||
|
get_status
|
||||||
|
;;
|
||||||
|
get_config)
|
||||||
|
get_config
|
||||||
|
;;
|
||||||
|
get_web_url)
|
||||||
|
get_web_url
|
||||||
|
;;
|
||||||
|
install_mmpm)
|
||||||
|
install_mmpm
|
||||||
|
;;
|
||||||
|
update_mmpm)
|
||||||
|
update_mmpm
|
||||||
|
;;
|
||||||
|
service_start)
|
||||||
|
service_start
|
||||||
|
;;
|
||||||
|
service_stop)
|
||||||
|
service_stop
|
||||||
|
;;
|
||||||
|
service_restart)
|
||||||
|
service_restart
|
||||||
|
;;
|
||||||
|
search_modules)
|
||||||
|
read -r input
|
||||||
|
query=$(echo "$input" | jsonfilter -e '@.query' 2>/dev/null)
|
||||||
|
search_modules "$query"
|
||||||
|
;;
|
||||||
|
list_modules)
|
||||||
|
list_modules
|
||||||
|
;;
|
||||||
|
install_module)
|
||||||
|
read -r input
|
||||||
|
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||||
|
install_module "$name"
|
||||||
|
;;
|
||||||
|
remove_module)
|
||||||
|
read -r input
|
||||||
|
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||||
|
remove_module "$name"
|
||||||
|
;;
|
||||||
|
upgrade_modules)
|
||||||
|
read -r input
|
||||||
|
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||||
|
upgrade_modules "$name"
|
||||||
|
;;
|
||||||
|
set_config)
|
||||||
|
read -r input
|
||||||
|
key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null)
|
||||||
|
value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null)
|
||||||
|
set_config "$key" "$value"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"error":"Unknown method"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"error":"Unknown command"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"admin/secubox/services/mmpm": {
|
||||||
|
"title": "MMPM",
|
||||||
|
"order": 32,
|
||||||
|
"action": {
|
||||||
|
"type": "alias",
|
||||||
|
"path": "admin/secubox/services/mmpm/dashboard"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": ["luci-app-mmpm"],
|
||||||
|
"uci": {"mmpm": true}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/secubox/services/mmpm/dashboard": {
|
||||||
|
"title": "Dashboard",
|
||||||
|
"order": 1,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "mmpm/dashboard"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/secubox/services/mmpm/modules": {
|
||||||
|
"title": "Modules",
|
||||||
|
"order": 2,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "mmpm/modules"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/secubox/services/mmpm/webui": {
|
||||||
|
"title": "Web GUI",
|
||||||
|
"order": 3,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "mmpm/webui"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/secubox/services/mmpm/settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"order": 4,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "mmpm/settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"luci-app-mmpm": {
|
||||||
|
"description": "Grant access to MMPM",
|
||||||
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.mmpm": ["get_status", "get_config", "get_web_url", "list_modules", "search_modules"]
|
||||||
|
},
|
||||||
|
"uci": ["mmpm"]
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.mmpm": ["install_mmpm", "update_mmpm", "service_start", "service_stop", "service_restart", "install_module", "remove_module", "upgrade_modules", "set_config"]
|
||||||
|
},
|
||||||
|
"uci": ["mmpm"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,8 +10,8 @@ LUCI_TITLE:=SecuBox Portal - Unified WebUI
|
|||||||
LUCI_DESCRIPTION:=Unified entry point for all SecuBox applications with tabbed navigation
|
LUCI_DESCRIPTION:=Unified entry point for all SecuBox applications with tabbed navigation
|
||||||
LUCI_DEPENDS:=+luci-base +luci-theme-secubox
|
LUCI_DEPENDS:=+luci-base +luci-theme-secubox
|
||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
PKG_VERSION:=1.0.2
|
PKG_VERSION:=0.6.0
|
||||||
PKG_RELEASE:=2
|
PKG_RELEASE:=1
|
||||||
PKG_LICENSE:=GPL-3.0-or-later
|
PKG_LICENSE:=GPL-3.0-or-later
|
||||||
PKG_MAINTAINER:=SecuBox Team <secubox@example.com>
|
PKG_MAINTAINER:=SecuBox Team <secubox@example.com>
|
||||||
|
|
||||||
|
|||||||
@ -211,6 +211,30 @@ return baseclass.extend({
|
|||||||
path: 'admin/secubox/services/vhosts/overview',
|
path: 'admin/secubox/services/vhosts/overview',
|
||||||
service: 'nginx',
|
service: 'nginx',
|
||||||
version: '0.5.0'
|
version: '0.5.0'
|
||||||
|
},
|
||||||
|
'magicmirror2': {
|
||||||
|
id: 'magicmirror2',
|
||||||
|
name: 'MagicMirror²',
|
||||||
|
desc: 'Smart display platform with modular widgets for weather, calendar, news and more',
|
||||||
|
icon: '\ud83e\ude9e',
|
||||||
|
iconBg: 'rgba(155, 89, 182, 0.15)',
|
||||||
|
iconColor: '#9b59b6',
|
||||||
|
section: 'services',
|
||||||
|
path: 'admin/secubox/services/magicmirror2/dashboard',
|
||||||
|
service: 'magicmirror2',
|
||||||
|
version: '2.29.0'
|
||||||
|
},
|
||||||
|
'mmpm': {
|
||||||
|
id: 'mmpm',
|
||||||
|
name: 'MMPM',
|
||||||
|
desc: 'MagicMirror Package Manager - Install, update and manage modules easily',
|
||||||
|
icon: '\ud83d\udce6',
|
||||||
|
iconBg: 'rgba(243, 156, 18, 0.15)',
|
||||||
|
iconColor: '#f39c12',
|
||||||
|
section: 'services',
|
||||||
|
path: 'admin/secubox/services/mmpm/dashboard',
|
||||||
|
service: 'mmpm',
|
||||||
|
version: '3.1.0'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
73
package/secubox/secubox-app-magicmirror2/Makefile
Normal file
73
package/secubox/secubox-app-magicmirror2/Makefile
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=secubox-app-magicmirror2
|
||||||
|
PKG_RELEASE:=5
|
||||||
|
PKG_VERSION:=0.4.0
|
||||||
|
PKG_ARCH:=all
|
||||||
|
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
|
||||||
|
PKG_LICENSE:=Apache-2.0
|
||||||
|
|
||||||
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
|
||||||
|
define Package/secubox-app-magicmirror2
|
||||||
|
SECTION:=utils
|
||||||
|
CATEGORY:=Utilities
|
||||||
|
PKGARCH:=all
|
||||||
|
SUBMENU:=SecuBox Apps
|
||||||
|
TITLE:=SecuBox MagicMirror2 Smart Display Platform (LXC)
|
||||||
|
DEPENDS:=+uci +libuci +wget +tar +jq +zstd
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-magicmirror2/description
|
||||||
|
MagicMirror² - Open source modular smart mirror platform for SecuBox.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Modular architecture with hundreds of available modules
|
||||||
|
- Built-in module manager for easy installation
|
||||||
|
- Weather, calendar, news, and custom widgets
|
||||||
|
- Web-based configuration interface
|
||||||
|
- Kiosk mode for dedicated displays
|
||||||
|
|
||||||
|
Runs in LXC container for isolation and security.
|
||||||
|
Configure in /etc/config/magicmirror2.
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-magicmirror2/conffiles
|
||||||
|
/etc/config/magicmirror2
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Build/Compile
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-magicmirror2/install
|
||||||
|
$(INSTALL_DIR) $(1)/etc/config
|
||||||
|
$(INSTALL_CONF) ./files/etc/config/magicmirror2 $(1)/etc/config/magicmirror2
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/etc/init.d
|
||||||
|
$(INSTALL_BIN) ./files/etc/init.d/magicmirror2 $(1)/etc/init.d/magicmirror2
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/sbin
|
||||||
|
$(INSTALL_BIN) ./files/usr/sbin/mm2ctl $(1)/usr/sbin/mm2ctl
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-magicmirror2/postinst
|
||||||
|
#!/bin/sh
|
||||||
|
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||||
|
echo ""
|
||||||
|
echo "MagicMirror2 installed."
|
||||||
|
echo ""
|
||||||
|
echo "To install and start MagicMirror2:"
|
||||||
|
echo " mm2ctl install"
|
||||||
|
echo " /etc/init.d/magicmirror2 start"
|
||||||
|
echo ""
|
||||||
|
echo "Web interface: http://<router-ip>:8082"
|
||||||
|
echo ""
|
||||||
|
echo "To manage modules:"
|
||||||
|
echo " mm2ctl module list"
|
||||||
|
echo " mm2ctl module install MMM-<name>"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,secubox-app-magicmirror2))
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
# MagicMirror2 configuration for SecuBox
|
||||||
|
|
||||||
|
config magicmirror2 'main'
|
||||||
|
option enabled '0'
|
||||||
|
option port '8082'
|
||||||
|
option address '0.0.0.0'
|
||||||
|
option data_path '/srv/magicmirror2'
|
||||||
|
option memory_limit '512M'
|
||||||
|
option language 'en'
|
||||||
|
option timezone 'Europe/Paris'
|
||||||
|
option units 'metric'
|
||||||
|
option electron_enabled '0'
|
||||||
|
|
||||||
|
config display 'display'
|
||||||
|
option width '1920'
|
||||||
|
option height '1080'
|
||||||
|
option zoom '1.0'
|
||||||
|
option brightness '100'
|
||||||
|
|
||||||
|
config weather 'weather'
|
||||||
|
option enabled '0'
|
||||||
|
option provider 'openweathermap'
|
||||||
|
option api_key ''
|
||||||
|
option location ''
|
||||||
|
option location_id ''
|
||||||
|
option units 'metric'
|
||||||
|
|
||||||
|
config calendar 'calendar'
|
||||||
|
option enabled '0'
|
||||||
|
option max_entries '10'
|
||||||
|
option fetch_interval '300000'
|
||||||
|
|
||||||
|
config newsfeed 'newsfeed'
|
||||||
|
option enabled '0'
|
||||||
|
option show_description '1'
|
||||||
|
option show_source_title '1'
|
||||||
|
option max_news_items '5'
|
||||||
|
|
||||||
|
config clock 'clock'
|
||||||
|
option enabled '1'
|
||||||
|
option display_seconds '1'
|
||||||
|
option show_date '1'
|
||||||
|
option show_week '0'
|
||||||
|
option date_format 'dddd, LL'
|
||||||
|
|
||||||
|
config compliments 'compliments'
|
||||||
|
option enabled '1'
|
||||||
|
option update_interval '30000'
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/sh /etc/rc.common
|
||||||
|
# SecuBox MagicMirror2 service
|
||||||
|
|
||||||
|
START=95
|
||||||
|
STOP=10
|
||||||
|
USE_PROCD=1
|
||||||
|
|
||||||
|
PROG=/usr/sbin/mm2ctl
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
local enabled
|
||||||
|
config_load magicmirror2
|
||||||
|
config_get enabled main enabled '0'
|
||||||
|
|
||||||
|
[ "$enabled" = "1" ] || return 0
|
||||||
|
|
||||||
|
procd_open_instance
|
||||||
|
procd_set_param command "$PROG" service-run
|
||||||
|
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
|
||||||
|
procd_set_param stdout 1
|
||||||
|
procd_set_param stderr 1
|
||||||
|
procd_close_instance
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
"$PROG" service-stop
|
||||||
|
}
|
||||||
|
|
||||||
|
reload_service() {
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
service_triggers() {
|
||||||
|
procd_add_reload_trigger "magicmirror2"
|
||||||
|
}
|
||||||
913
package/secubox/secubox-app-magicmirror2/files/usr/sbin/mm2ctl
Normal file
913
package/secubox/secubox-app-magicmirror2/files/usr/sbin/mm2ctl
Normal file
@ -0,0 +1,913 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SecuBox MagicMirror2 manager - LXC container support with module management
|
||||||
|
# Copyright (C) 2024-2026 CyberMind.fr
|
||||||
|
|
||||||
|
CONFIG="magicmirror2"
|
||||||
|
LXC_NAME="magicmirror2"
|
||||||
|
OPKG_UPDATED=0
|
||||||
|
|
||||||
|
# Paths
|
||||||
|
LXC_PATH="/srv/lxc"
|
||||||
|
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
|
||||||
|
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
|
||||||
|
|
||||||
|
# MagicMirror paths inside container
|
||||||
|
MM_PATH="/opt/magic_mirror"
|
||||||
|
MM_MODULES="$MM_PATH/modules"
|
||||||
|
MM_CONFIG="$MM_PATH/config"
|
||||||
|
|
||||||
|
# Third-party modules registry
|
||||||
|
MM_REGISTRY_URL="https://raw.githubusercontent.com/MagicMirrorOrg/MagicMirror-3rd-Party-Modules/master/modules.json"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: mm2ctl <command> [options]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
install Install prerequisites and create LXC container
|
||||||
|
update Update MagicMirror2 in container
|
||||||
|
status Show container and service status
|
||||||
|
logs Show MagicMirror2 logs (use -f to follow)
|
||||||
|
shell Open shell in container
|
||||||
|
config Generate/update config.js from UCI
|
||||||
|
service-run Internal: run container under procd
|
||||||
|
service-stop Stop container
|
||||||
|
|
||||||
|
Module Management:
|
||||||
|
module list List installed modules
|
||||||
|
module available [search] List available third-party modules
|
||||||
|
module install <name|url> Install a module (MMM-name or git URL)
|
||||||
|
module remove <name> Remove an installed module
|
||||||
|
module update [name] Update module(s)
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
set <key> <value> Set UCI configuration value
|
||||||
|
get <key> Get UCI configuration value
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
mm2ctl install
|
||||||
|
mm2ctl module install MMM-WeatherChart
|
||||||
|
mm2ctl module install https://github.com/user/MMM-Custom
|
||||||
|
mm2ctl module list
|
||||||
|
mm2ctl config
|
||||||
|
|
||||||
|
Web Interface: http://<router-ip>:8082
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
require_root() { [ "$(id -u)" -eq 0 ] || { echo "Root required" >&2; exit 1; }; }
|
||||||
|
|
||||||
|
log_info() { echo "[INFO] $*"; }
|
||||||
|
log_warn() { echo "[WARN] $*" >&2; }
|
||||||
|
log_error() { echo "[ERROR] $*" >&2; }
|
||||||
|
|
||||||
|
uci_get() { uci -q get ${CONFIG}.$1; }
|
||||||
|
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
|
||||||
|
|
||||||
|
# Load configuration with defaults
|
||||||
|
load_config() {
|
||||||
|
port="$(uci_get main.port || echo 8082)"
|
||||||
|
address="$(uci_get main.address || echo 0.0.0.0)"
|
||||||
|
data_path="$(uci_get main.data_path || echo /srv/magicmirror2)"
|
||||||
|
memory_limit="$(uci_get main.memory_limit || echo 512M)"
|
||||||
|
language="$(uci_get main.language || echo en)"
|
||||||
|
timezone="$(uci_get main.timezone || echo Europe/Paris)"
|
||||||
|
units="$(uci_get main.units || echo metric)"
|
||||||
|
electron_enabled="$(uci_get main.electron_enabled || echo 0)"
|
||||||
|
|
||||||
|
# Display settings
|
||||||
|
display_width="$(uci_get display.width || echo 1920)"
|
||||||
|
display_height="$(uci_get display.height || echo 1080)"
|
||||||
|
display_zoom="$(uci_get display.zoom || echo 1.0)"
|
||||||
|
|
||||||
|
# Weather settings
|
||||||
|
weather_enabled="$(uci_get weather.enabled || echo 0)"
|
||||||
|
weather_provider="$(uci_get weather.provider || echo openweathermap)"
|
||||||
|
weather_api_key="$(uci_get weather.api_key || echo '')"
|
||||||
|
weather_location="$(uci_get weather.location || echo '')"
|
||||||
|
weather_location_id="$(uci_get weather.location_id || echo '')"
|
||||||
|
|
||||||
|
# Clock settings
|
||||||
|
clock_enabled="$(uci_get clock.enabled || echo 1)"
|
||||||
|
clock_display_seconds="$(uci_get clock.display_seconds || echo 1)"
|
||||||
|
clock_show_date="$(uci_get clock.show_date || echo 1)"
|
||||||
|
|
||||||
|
# Calendar settings
|
||||||
|
calendar_enabled="$(uci_get calendar.enabled || echo 0)"
|
||||||
|
calendar_max_entries="$(uci_get calendar.max_entries || echo 10)"
|
||||||
|
|
||||||
|
# Newsfeed settings
|
||||||
|
newsfeed_enabled="$(uci_get newsfeed.enabled || echo 0)"
|
||||||
|
newsfeed_max_items="$(uci_get newsfeed.max_news_items || echo 5)"
|
||||||
|
|
||||||
|
# Compliments settings
|
||||||
|
compliments_enabled="$(uci_get compliments.enabled || echo 1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
|
||||||
|
|
||||||
|
has_lxc() {
|
||||||
|
command -v lxc-start >/dev/null 2>&1 && \
|
||||||
|
command -v lxc-stop >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure required packages are installed
|
||||||
|
ensure_packages() {
|
||||||
|
require_root
|
||||||
|
for pkg in "$@"; do
|
||||||
|
if ! opkg list-installed | grep -q "^$pkg "; then
|
||||||
|
if [ "$OPKG_UPDATED" -eq 0 ]; then
|
||||||
|
opkg update || return 1
|
||||||
|
OPKG_UPDATED=1
|
||||||
|
fi
|
||||||
|
opkg install "$pkg" || return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# LXC CONTAINER FUNCTIONS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
lxc_check_prereqs() {
|
||||||
|
log_info "Checking LXC prerequisites..."
|
||||||
|
ensure_packages lxc lxc-common lxc-attach lxc-start lxc-stop lxc-destroy || return 1
|
||||||
|
|
||||||
|
if [ ! -d /sys/fs/cgroup ]; then
|
||||||
|
log_error "cgroups not mounted at /sys/fs/cgroup"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "LXC ready"
|
||||||
|
}
|
||||||
|
|
||||||
|
lxc_create_rootfs() {
|
||||||
|
load_config
|
||||||
|
|
||||||
|
if [ -d "$LXC_ROOTFS" ] && [ -d "$LXC_ROOTFS/opt/magic_mirror" ]; then
|
||||||
|
log_info "LXC rootfs already exists with MagicMirror2"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Creating LXC rootfs for MagicMirror2..."
|
||||||
|
ensure_dir "$LXC_PATH/$LXC_NAME"
|
||||||
|
|
||||||
|
lxc_create_docker_rootfs || return 1
|
||||||
|
lxc_create_config || return 1
|
||||||
|
|
||||||
|
log_info "LXC rootfs created successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
lxc_create_docker_rootfs() {
|
||||||
|
local rootfs="$LXC_ROOTFS"
|
||||||
|
local image="karsten13/magicmirror"
|
||||||
|
local tag="latest"
|
||||||
|
local registry="registry-1.docker.io"
|
||||||
|
local arch
|
||||||
|
local tmp_layer="/tmp/mm2_layer.tar"
|
||||||
|
|
||||||
|
# Detect architecture for Docker manifest
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64) arch="amd64" ;;
|
||||||
|
aarch64) arch="arm64" ;;
|
||||||
|
armv7l) arch="arm" ;;
|
||||||
|
*) arch="amd64" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
log_info "Extracting MagicMirror2 Docker image ($arch)..."
|
||||||
|
ensure_dir "$rootfs"
|
||||||
|
|
||||||
|
# Get Docker Hub token
|
||||||
|
local token=$(wget -q -O - "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" | jsonfilter -e '@.token')
|
||||||
|
[ -z "$token" ] && { log_error "Failed to get Docker Hub token"; return 1; }
|
||||||
|
|
||||||
|
# Get manifest list
|
||||||
|
local manifest=$(wget -q -O - --header="Authorization: Bearer $token" \
|
||||||
|
--header="Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
|
||||||
|
"https://$registry/v2/$image/manifests/$tag")
|
||||||
|
|
||||||
|
# Find digest for our architecture
|
||||||
|
local digest=$(echo "$manifest" | jsonfilter -e "@.manifests[@.platform.architecture='$arch'].digest")
|
||||||
|
[ -z "$digest" ] && { log_error "No manifest found for $arch"; return 1; }
|
||||||
|
|
||||||
|
# Get image manifest
|
||||||
|
local img_manifest=$(wget -q -O - --header="Authorization: Bearer $token" \
|
||||||
|
--header="Accept: application/vnd.docker.distribution.manifest.v2+json" \
|
||||||
|
"https://$registry/v2/$image/manifests/$digest")
|
||||||
|
|
||||||
|
# Extract layers and download them
|
||||||
|
log_info "Downloading and extracting layers..."
|
||||||
|
local layers=$(echo "$img_manifest" | jsonfilter -e '@.layers[*].digest')
|
||||||
|
|
||||||
|
for layer_digest in $layers; do
|
||||||
|
log_info " Layer: ${layer_digest:7:12}..."
|
||||||
|
|
||||||
|
# Download layer to temp file
|
||||||
|
wget -q -O "$tmp_layer" --header="Authorization: Bearer $token" \
|
||||||
|
"https://$registry/v2/$image/blobs/$layer_digest" || {
|
||||||
|
log_warn " Failed to download layer"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try decompression methods in order (gzip most common, then zstd, then plain tar)
|
||||||
|
# Method 1: Try gzip
|
||||||
|
if gunzip -t "$tmp_layer" 2>/dev/null; then
|
||||||
|
gunzip -c "$tmp_layer" | tar xf - -C "$rootfs" 2>/dev/null || true
|
||||||
|
# Method 2: Try zstd
|
||||||
|
elif command -v zstd >/dev/null 2>&1 && zstd -t "$tmp_layer" 2>/dev/null; then
|
||||||
|
zstd -d -c "$tmp_layer" | tar xf - -C "$rootfs" 2>/dev/null || true
|
||||||
|
# Method 3: Try plain tar
|
||||||
|
elif tar tf "$tmp_layer" >/dev/null 2>&1; then
|
||||||
|
tar xf "$tmp_layer" -C "$rootfs" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
# Last resort: try zstd even if test failed (might need to install it)
|
||||||
|
if ! command -v zstd >/dev/null 2>&1; then
|
||||||
|
log_warn " Installing zstd for compressed layers..."
|
||||||
|
opkg update >/dev/null 2>&1 && opkg install zstd >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
if command -v zstd >/dev/null 2>&1; then
|
||||||
|
zstd -d -c "$tmp_layer" 2>/dev/null | tar xf - -C "$rootfs" 2>/dev/null || \
|
||||||
|
gunzip -c "$tmp_layer" 2>/dev/null | tar xf - -C "$rootfs" 2>/dev/null || \
|
||||||
|
tar xf "$tmp_layer" -C "$rootfs" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -f "$tmp_layer"
|
||||||
|
|
||||||
|
# Configure container
|
||||||
|
echo "nameserver 8.8.8.8" > "$rootfs/etc/resolv.conf"
|
||||||
|
mkdir -p "$rootfs/opt/magic_mirror/config" "$rootfs/opt/magic_mirror/modules" "$rootfs/tmp"
|
||||||
|
|
||||||
|
# Ensure proper shell setup
|
||||||
|
log_info "Checking shell availability..."
|
||||||
|
if [ ! -x "$rootfs/bin/sh" ]; then
|
||||||
|
if [ -x "$rootfs/bin/bash" ]; then
|
||||||
|
ln -sf bash "$rootfs/bin/sh"
|
||||||
|
elif [ -x "$rootfs/bin/dash" ]; then
|
||||||
|
ln -sf dash "$rootfs/bin/sh"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create startup script
|
||||||
|
cat > "$rootfs/opt/start-mm2.sh" << 'START'
|
||||||
|
#!/bin/sh
|
||||||
|
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"
|
||||||
|
export NODE_ENV=production
|
||||||
|
export MM_PORT="${MM2_PORT:-8082}"
|
||||||
|
export MM_ADDRESS="${MM2_ADDRESS:-0.0.0.0}"
|
||||||
|
|
||||||
|
cd /opt/magic_mirror
|
||||||
|
|
||||||
|
# Wait for config to be available
|
||||||
|
for i in 1 2 3 4 5; do
|
||||||
|
[ -f /opt/magic_mirror/config/config.js ] && break
|
||||||
|
echo "Waiting for config.js..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ! -f /opt/magic_mirror/config/config.js ]; then
|
||||||
|
echo "ERROR: config.js not found, using default"
|
||||||
|
cp /opt/magic_mirror/config/config.js.sample /opt/magic_mirror/config/config.js 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting MagicMirror2 on port $MM_PORT..."
|
||||||
|
|
||||||
|
# Run MagicMirror in server-only mode
|
||||||
|
exec npm run server
|
||||||
|
START
|
||||||
|
chmod +x "$rootfs/opt/start-mm2.sh"
|
||||||
|
|
||||||
|
log_info "MagicMirror2 Docker image extracted successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
lxc_create_config() {
|
||||||
|
load_config
|
||||||
|
|
||||||
|
cat > "$LXC_CONFIG" << EOF
|
||||||
|
# MagicMirror2 LXC Configuration
|
||||||
|
lxc.uts.name = $LXC_NAME
|
||||||
|
|
||||||
|
# Root filesystem
|
||||||
|
lxc.rootfs.path = dir:$LXC_ROOTFS
|
||||||
|
|
||||||
|
# Network - use host network for simplicity
|
||||||
|
lxc.net.0.type = none
|
||||||
|
|
||||||
|
# Mounts
|
||||||
|
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
|
||||||
|
lxc.mount.entry = $data_path/config opt/magic_mirror/config none bind,create=dir 0 0
|
||||||
|
lxc.mount.entry = $data_path/modules opt/magic_mirror/modules none bind,create=dir 0 0
|
||||||
|
lxc.mount.entry = $data_path/css opt/magic_mirror/css/custom none bind,create=dir 0 0
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
lxc.environment = MM2_PORT=$port
|
||||||
|
lxc.environment = MM2_ADDRESS=$address
|
||||||
|
lxc.environment = TZ=$timezone
|
||||||
|
lxc.environment = NODE_ENV=production
|
||||||
|
|
||||||
|
# Capabilities
|
||||||
|
lxc.cap.drop = sys_admin sys_module mac_admin mac_override
|
||||||
|
|
||||||
|
# cgroups limits
|
||||||
|
lxc.cgroup.memory.limit_in_bytes = $memory_limit
|
||||||
|
|
||||||
|
# Init
|
||||||
|
lxc.init.cmd = /opt/start-mm2.sh
|
||||||
|
|
||||||
|
# Console
|
||||||
|
lxc.console.size = 1024
|
||||||
|
lxc.pty.max = 1024
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_info "LXC config created at $LXC_CONFIG"
|
||||||
|
}
|
||||||
|
|
||||||
|
lxc_stop() {
|
||||||
|
if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then
|
||||||
|
lxc-stop -n "$LXC_NAME" -k >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
lxc_run() {
|
||||||
|
load_config
|
||||||
|
lxc_stop
|
||||||
|
|
||||||
|
if [ ! -f "$LXC_CONFIG" ]; then
|
||||||
|
log_error "LXC not configured. Run 'mm2ctl install' first."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Regenerate config to pick up UCI changes
|
||||||
|
lxc_create_config
|
||||||
|
|
||||||
|
# Ensure mount points exist
|
||||||
|
ensure_dir "$data_path/config"
|
||||||
|
ensure_dir "$data_path/modules"
|
||||||
|
ensure_dir "$data_path/css"
|
||||||
|
|
||||||
|
# Generate MagicMirror config.js from UCI
|
||||||
|
generate_mm_config
|
||||||
|
|
||||||
|
log_info "Starting MagicMirror2 LXC container..."
|
||||||
|
log_info "Web interface: http://0.0.0.0:$port"
|
||||||
|
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
|
||||||
|
}
|
||||||
|
|
||||||
|
lxc_status() {
|
||||||
|
load_config
|
||||||
|
echo "=== MagicMirror2 Status ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then
|
||||||
|
lxc-info -n "$LXC_NAME"
|
||||||
|
else
|
||||||
|
echo "LXC container '$LXC_NAME' not found or not configured"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Configuration ==="
|
||||||
|
echo "Port: $port"
|
||||||
|
echo "Address: $address"
|
||||||
|
echo "Data path: $data_path"
|
||||||
|
echo "Language: $language"
|
||||||
|
echo "Timezone: $timezone"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Installed Modules ==="
|
||||||
|
list_installed_modules
|
||||||
|
}
|
||||||
|
|
||||||
|
lxc_logs() {
|
||||||
|
if lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
if [ "$1" = "-f" ]; then
|
||||||
|
logread -f -e magicmirror2
|
||||||
|
else
|
||||||
|
logread -e magicmirror2 | tail -100
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warn "Container not running. Try: logread -e magicmirror2"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
lxc_shell() {
|
||||||
|
lxc-attach -n "$LXC_NAME" -- /bin/sh
|
||||||
|
}
|
||||||
|
|
||||||
|
lxc_destroy() {
|
||||||
|
lxc_stop
|
||||||
|
if [ -d "$LXC_PATH/$LXC_NAME" ]; then
|
||||||
|
rm -rf "$LXC_PATH/$LXC_NAME"
|
||||||
|
log_info "LXC container destroyed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MAGICMIRROR CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
generate_mm_config() {
|
||||||
|
load_config
|
||||||
|
local config_file="$data_path/config/config.js"
|
||||||
|
|
||||||
|
log_info "Generating MagicMirror config.js..."
|
||||||
|
|
||||||
|
cat > "$config_file" << CONFIGJS
|
||||||
|
/* MagicMirror² Config - Generated by SecuBox mm2ctl */
|
||||||
|
let config = {
|
||||||
|
address: "$address",
|
||||||
|
port: $port,
|
||||||
|
basePath: "/",
|
||||||
|
ipWhitelist: [],
|
||||||
|
useHttps: false,
|
||||||
|
httpsPrivateKey: "",
|
||||||
|
httpsCertificate: "",
|
||||||
|
|
||||||
|
language: "$language",
|
||||||
|
locale: "$language",
|
||||||
|
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
|
||||||
|
timeFormat: 24,
|
||||||
|
units: "$units",
|
||||||
|
|
||||||
|
modules: [
|
||||||
|
CONFIGJS
|
||||||
|
|
||||||
|
# Add clock module
|
||||||
|
if [ "$clock_enabled" = "1" ]; then
|
||||||
|
cat >> "$config_file" << CONFIGJS
|
||||||
|
{
|
||||||
|
module: "clock",
|
||||||
|
position: "top_left",
|
||||||
|
config: {
|
||||||
|
displaySeconds: $([ "$clock_display_seconds" = "1" ] && echo "true" || echo "false"),
|
||||||
|
showDate: $([ "$clock_show_date" = "1" ] && echo "true" || echo "false"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CONFIGJS
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add weather module
|
||||||
|
if [ "$weather_enabled" = "1" ] && [ -n "$weather_api_key" ]; then
|
||||||
|
cat >> "$config_file" << CONFIGJS
|
||||||
|
{
|
||||||
|
module: "weather",
|
||||||
|
position: "top_right",
|
||||||
|
config: {
|
||||||
|
weatherProvider: "$weather_provider",
|
||||||
|
type: "current",
|
||||||
|
location: "$weather_location",
|
||||||
|
locationID: "$weather_location_id",
|
||||||
|
apiKey: "$weather_api_key",
|
||||||
|
units: "$units"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
module: "weather",
|
||||||
|
position: "top_right",
|
||||||
|
header: "Weather Forecast",
|
||||||
|
config: {
|
||||||
|
weatherProvider: "$weather_provider",
|
||||||
|
type: "forecast",
|
||||||
|
location: "$weather_location",
|
||||||
|
locationID: "$weather_location_id",
|
||||||
|
apiKey: "$weather_api_key",
|
||||||
|
units: "$units"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CONFIGJS
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add calendar module
|
||||||
|
if [ "$calendar_enabled" = "1" ]; then
|
||||||
|
cat >> "$config_file" << CONFIGJS
|
||||||
|
{
|
||||||
|
module: "calendar",
|
||||||
|
header: "Calendar",
|
||||||
|
position: "top_left",
|
||||||
|
config: {
|
||||||
|
maximumEntries: $calendar_max_entries,
|
||||||
|
calendars: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CONFIGJS
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add newsfeed module
|
||||||
|
if [ "$newsfeed_enabled" = "1" ]; then
|
||||||
|
cat >> "$config_file" << CONFIGJS
|
||||||
|
{
|
||||||
|
module: "newsfeed",
|
||||||
|
position: "bottom_bar",
|
||||||
|
config: {
|
||||||
|
feeds: [
|
||||||
|
{
|
||||||
|
title: "BBC News",
|
||||||
|
url: "https://feeds.bbci.co.uk/news/rss.xml"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
showSourceTitle: true,
|
||||||
|
showPublishDate: true,
|
||||||
|
broadcastNewsFeeds: true,
|
||||||
|
broadcastNewsUpdates: true,
|
||||||
|
maxNewsItems: $newsfeed_max_items
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CONFIGJS
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add compliments module
|
||||||
|
if [ "$compliments_enabled" = "1" ]; then
|
||||||
|
cat >> "$config_file" << CONFIGJS
|
||||||
|
{
|
||||||
|
module: "compliments",
|
||||||
|
position: "lower_third",
|
||||||
|
config: {
|
||||||
|
compliments: {
|
||||||
|
anytime: ["Welcome to SecuBox MagicMirror!"],
|
||||||
|
morning: ["Good morning!"],
|
||||||
|
afternoon: ["Good afternoon!"],
|
||||||
|
evening: ["Good evening!"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CONFIGJS
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load custom modules from data directory
|
||||||
|
if [ -d "$data_path/modules" ]; then
|
||||||
|
for module_dir in "$data_path/modules"/MMM-*; do
|
||||||
|
if [ -d "$module_dir" ] && [ -f "$module_dir/package.json" ]; then
|
||||||
|
local module_name=$(basename "$module_dir")
|
||||||
|
# Check if module has a config file
|
||||||
|
if [ -f "$data_path/config/${module_name}.json" ]; then
|
||||||
|
local module_config=$(cat "$data_path/config/${module_name}.json")
|
||||||
|
cat >> "$config_file" << CONFIGJS
|
||||||
|
{
|
||||||
|
module: "$module_name",
|
||||||
|
position: "top_center",
|
||||||
|
config: $module_config
|
||||||
|
},
|
||||||
|
CONFIGJS
|
||||||
|
else
|
||||||
|
cat >> "$config_file" << CONFIGJS
|
||||||
|
{
|
||||||
|
module: "$module_name",
|
||||||
|
position: "top_center"
|
||||||
|
},
|
||||||
|
CONFIGJS
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Close config
|
||||||
|
cat >> "$config_file" << CONFIGJS
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||||
|
if (typeof module !== "undefined") {module.exports = config;}
|
||||||
|
CONFIGJS
|
||||||
|
|
||||||
|
log_info "Config generated at $config_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MODULE MANAGEMENT
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
list_installed_modules() {
|
||||||
|
load_config
|
||||||
|
local modules_dir="$data_path/modules"
|
||||||
|
|
||||||
|
if [ ! -d "$modules_dir" ]; then
|
||||||
|
echo "No modules directory found"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
# List MMM-* modules
|
||||||
|
for module_dir in "$modules_dir"/MMM-*; do
|
||||||
|
[ -d "$module_dir" ] || continue
|
||||||
|
[ -f "$module_dir/package.json" ] || continue
|
||||||
|
local name=$(basename "$module_dir")
|
||||||
|
local version=$(jsonfilter -i "$module_dir/package.json" -e '@.version' 2>/dev/null || echo "unknown")
|
||||||
|
local desc=$(jsonfilter -i "$module_dir/package.json" -e '@.description' 2>/dev/null | head -c 60)
|
||||||
|
printf " %-30s v%-10s %s\n" "$name" "$version" "$desc"
|
||||||
|
done
|
||||||
|
# List mm-* modules
|
||||||
|
for module_dir in "$modules_dir"/mm-*; do
|
||||||
|
[ -d "$module_dir" ] || continue
|
||||||
|
[ -f "$module_dir/package.json" ] || continue
|
||||||
|
local name=$(basename "$module_dir")
|
||||||
|
local version=$(jsonfilter -i "$module_dir/package.json" -e '@.version' 2>/dev/null || echo "unknown")
|
||||||
|
local desc=$(jsonfilter -i "$module_dir/package.json" -e '@.description' 2>/dev/null | head -c 60)
|
||||||
|
printf " %-30s v%-10s %s\n" "$name" "$version" "$desc"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
list_available_modules() {
|
||||||
|
local search="${1:-}"
|
||||||
|
local cache_file="/tmp/mm2_modules_cache.json"
|
||||||
|
|
||||||
|
# Download registry if not cached or old
|
||||||
|
if [ ! -f "$cache_file" ] || [ $(find "$cache_file" -mmin +60 2>/dev/null | wc -l) -gt 0 ]; then
|
||||||
|
log_info "Fetching module registry..."
|
||||||
|
wget -q -O "$cache_file" "$MM_REGISTRY_URL" || {
|
||||||
|
log_error "Failed to fetch module registry"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Available third-party modules:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ -n "$search" ]; then
|
||||||
|
grep -i "$search" "$cache_file" | head -50 || echo "No modules matching '$search'"
|
||||||
|
else
|
||||||
|
# Show first 30 modules
|
||||||
|
jsonfilter -i "$cache_file" -e '@[*].title' 2>/dev/null | head -30 | while read title; do
|
||||||
|
echo " $title"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo "Use 'mm2ctl module available <search>' to filter"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_module() {
|
||||||
|
local module_name="$1"
|
||||||
|
load_config
|
||||||
|
|
||||||
|
if [ -z "$module_name" ]; then
|
||||||
|
log_error "Module name required"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local modules_dir="$data_path/modules"
|
||||||
|
ensure_dir "$modules_dir"
|
||||||
|
|
||||||
|
local git_url=""
|
||||||
|
|
||||||
|
# Check if it's a URL
|
||||||
|
case "$module_name" in
|
||||||
|
http*|git@*)
|
||||||
|
git_url="$module_name"
|
||||||
|
module_name=$(basename "$git_url" .git)
|
||||||
|
;;
|
||||||
|
MMM-*|mm-*)
|
||||||
|
# Try to find in registry
|
||||||
|
git_url="https://github.com/MagicMirror-modules/$module_name"
|
||||||
|
# Also check MichMich's repos
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
module_name="MMM-$module_name"
|
||||||
|
git_url="https://github.com/MagicMirror-modules/$module_name"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -d "$modules_dir/$module_name" ]; then
|
||||||
|
log_warn "Module $module_name already installed"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Installing module: $module_name"
|
||||||
|
|
||||||
|
# Clone the module
|
||||||
|
if lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
# Use container's git
|
||||||
|
lxc-attach -n "$LXC_NAME" -- sh -c "cd /opt/magic_mirror/modules && git clone --depth 1 '$git_url' '$module_name'" || {
|
||||||
|
log_error "Failed to clone module"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install dependencies if package.json exists
|
||||||
|
if lxc-attach -n "$LXC_NAME" -- test -f "/opt/magic_mirror/modules/$module_name/package.json"; then
|
||||||
|
log_info "Installing module dependencies..."
|
||||||
|
lxc-attach -n "$LXC_NAME" -- sh -c "cd /opt/magic_mirror/modules/$module_name && npm install --production" || {
|
||||||
|
log_warn "Failed to install dependencies (module may still work)"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_error "Container not running. Start it first: /etc/init.d/magicmirror2 start"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Module $module_name installed successfully"
|
||||||
|
log_info "Restart MagicMirror2 to load the module: /etc/init.d/magicmirror2 restart"
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_module() {
|
||||||
|
local module_name="$1"
|
||||||
|
load_config
|
||||||
|
|
||||||
|
if [ -z "$module_name" ]; then
|
||||||
|
log_error "Module name required"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local module_path="$data_path/modules/$module_name"
|
||||||
|
|
||||||
|
if [ ! -d "$module_path" ]; then
|
||||||
|
log_error "Module $module_name not found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Removing module: $module_name"
|
||||||
|
rm -rf "$module_path"
|
||||||
|
|
||||||
|
# Remove config if exists
|
||||||
|
rm -f "$data_path/config/${module_name}.json"
|
||||||
|
|
||||||
|
log_info "Module $module_name removed"
|
||||||
|
log_info "Restart MagicMirror2 to apply: /etc/init.d/magicmirror2 restart"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_module() {
|
||||||
|
local module_name="$1"
|
||||||
|
load_config
|
||||||
|
|
||||||
|
if [ -z "$module_name" ]; then
|
||||||
|
# Update all modules
|
||||||
|
log_info "Updating all modules..."
|
||||||
|
# Update MMM-* modules
|
||||||
|
for module_dir in "$data_path/modules"/MMM-*; do
|
||||||
|
[ -d "$module_dir/.git" ] || continue
|
||||||
|
local name=$(basename "$module_dir")
|
||||||
|
log_info "Updating $name..."
|
||||||
|
lxc-attach -n "$LXC_NAME" -- sh -c "cd /opt/magic_mirror/modules/$name && git pull" || true
|
||||||
|
done
|
||||||
|
# Update mm-* modules
|
||||||
|
for module_dir in "$data_path/modules"/mm-*; do
|
||||||
|
[ -d "$module_dir/.git" ] || continue
|
||||||
|
local name=$(basename "$module_dir")
|
||||||
|
log_info "Updating $name..."
|
||||||
|
lxc-attach -n "$LXC_NAME" -- sh -c "cd /opt/magic_mirror/modules/$name && git pull" || true
|
||||||
|
done
|
||||||
|
else
|
||||||
|
local module_path="$data_path/modules/$module_name"
|
||||||
|
if [ ! -d "$module_path" ]; then
|
||||||
|
log_error "Module $module_name not found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Updating module: $module_name"
|
||||||
|
lxc-attach -n "$LXC_NAME" -- sh -c "cd /opt/magic_mirror/modules/$module_name && git pull" || {
|
||||||
|
log_error "Failed to update module"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Update complete. Restart MagicMirror2 to apply."
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# COMMANDS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
cmd_install() {
|
||||||
|
require_root
|
||||||
|
load_config
|
||||||
|
|
||||||
|
if ! has_lxc; then
|
||||||
|
log_error "LXC not available. Install lxc packages first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Installing MagicMirror2..."
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
ensure_dir "$data_path/config"
|
||||||
|
ensure_dir "$data_path/modules"
|
||||||
|
ensure_dir "$data_path/css"
|
||||||
|
|
||||||
|
lxc_check_prereqs || exit 1
|
||||||
|
lxc_create_rootfs || exit 1
|
||||||
|
|
||||||
|
# Generate initial config
|
||||||
|
generate_mm_config
|
||||||
|
|
||||||
|
uci_set main.enabled '1'
|
||||||
|
/etc/init.d/magicmirror2 enable
|
||||||
|
|
||||||
|
log_info "MagicMirror2 installed."
|
||||||
|
log_info "Start with: /etc/init.d/magicmirror2 start"
|
||||||
|
log_info "Web interface: http://<router-ip>:$port"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_update() {
|
||||||
|
require_root
|
||||||
|
load_config
|
||||||
|
|
||||||
|
log_info "Updating MagicMirror2..."
|
||||||
|
lxc_destroy
|
||||||
|
lxc_create_rootfs || exit 1
|
||||||
|
|
||||||
|
if /etc/init.d/magicmirror2 enabled >/dev/null 2>&1; then
|
||||||
|
/etc/init.d/magicmirror2 restart
|
||||||
|
else
|
||||||
|
log_info "Update complete. Restart manually to apply."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
lxc_status
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_logs() {
|
||||||
|
lxc_logs "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_shell() {
|
||||||
|
lxc_shell
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_config() {
|
||||||
|
generate_mm_config
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_module() {
|
||||||
|
local action="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
list)
|
||||||
|
list_installed_modules
|
||||||
|
;;
|
||||||
|
available)
|
||||||
|
list_available_modules "$@"
|
||||||
|
;;
|
||||||
|
install)
|
||||||
|
install_module "$@"
|
||||||
|
;;
|
||||||
|
remove|uninstall)
|
||||||
|
remove_module "$@"
|
||||||
|
;;
|
||||||
|
update)
|
||||||
|
update_module "$@"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown module action: $action"
|
||||||
|
echo "Use: list, available, install, remove, update"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_service_run() {
|
||||||
|
require_root
|
||||||
|
load_config
|
||||||
|
|
||||||
|
if ! has_lxc; then
|
||||||
|
log_error "LXC not available"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
lxc_check_prereqs || exit 1
|
||||||
|
lxc_run
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_service_stop() {
|
||||||
|
require_root
|
||||||
|
lxc_stop
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_set() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
|
||||||
|
if [ -z "$key" ] || [ -z "$value" ]; then
|
||||||
|
log_error "Usage: mm2ctl set <key> <value>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
uci_set "$key" "$value"
|
||||||
|
log_info "Set $key = $value"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_get() {
|
||||||
|
local key="$1"
|
||||||
|
|
||||||
|
if [ -z "$key" ]; then
|
||||||
|
log_error "Usage: mm2ctl get <key>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
uci_get "$key"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main Entry Point
|
||||||
|
case "${1:-}" in
|
||||||
|
install) shift; cmd_install "$@" ;;
|
||||||
|
update) shift; cmd_update "$@" ;;
|
||||||
|
status) shift; cmd_status "$@" ;;
|
||||||
|
logs) shift; cmd_logs "$@" ;;
|
||||||
|
shell) shift; cmd_shell "$@" ;;
|
||||||
|
config) shift; cmd_config "$@" ;;
|
||||||
|
module) shift; cmd_module "$@" ;;
|
||||||
|
set) shift; cmd_set "$@" ;;
|
||||||
|
get) shift; cmd_get "$@" ;;
|
||||||
|
service-run) shift; cmd_service_run "$@" ;;
|
||||||
|
service-stop) shift; cmd_service_stop "$@" ;;
|
||||||
|
help|--help|-h|'') usage ;;
|
||||||
|
*) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
67
package/secubox/secubox-app-mmpm/Makefile
Normal file
67
package/secubox/secubox-app-mmpm/Makefile
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=secubox-app-mmpm
|
||||||
|
PKG_VERSION:=0.2.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
PKG_ARCH:=all
|
||||||
|
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
|
||||||
|
PKG_LICENSE:=MIT
|
||||||
|
|
||||||
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
|
||||||
|
define Package/secubox-app-mmpm
|
||||||
|
SECTION:=utils
|
||||||
|
CATEGORY:=Utilities
|
||||||
|
PKGARCH:=all
|
||||||
|
SUBMENU:=SecuBox Apps
|
||||||
|
TITLE:=MMPM - MagicMirror Package Manager
|
||||||
|
DEPENDS:=+secubox-app-magicmirror2
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-mmpm/description
|
||||||
|
MMPM (MagicMirror Package Manager) for SecuBox.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Web-based GUI for module management
|
||||||
|
- Search MagicMirror module registry
|
||||||
|
- Install, update, remove modules easily
|
||||||
|
- Automatic dependency handling
|
||||||
|
- Module configuration interface
|
||||||
|
|
||||||
|
Runs inside the MagicMirror2 LXC container.
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-mmpm/conffiles
|
||||||
|
/etc/config/mmpm
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Build/Compile
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-mmpm/install
|
||||||
|
$(INSTALL_DIR) $(1)/etc/config
|
||||||
|
$(INSTALL_CONF) ./files/etc/config/mmpm $(1)/etc/config/mmpm
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/etc/init.d
|
||||||
|
$(INSTALL_BIN) ./files/etc/init.d/mmpm $(1)/etc/init.d/mmpm
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/sbin
|
||||||
|
$(INSTALL_BIN) ./files/usr/sbin/mmpmctl $(1)/usr/sbin/mmpmctl
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-mmpm/postinst
|
||||||
|
#!/bin/sh
|
||||||
|
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||||
|
echo ""
|
||||||
|
echo "MMPM installed."
|
||||||
|
echo ""
|
||||||
|
echo "To install MMPM in MagicMirror2 container:"
|
||||||
|
echo " mmpmctl install"
|
||||||
|
echo ""
|
||||||
|
echo "Web interface: http://<router-ip>:7891"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,secubox-app-mmpm))
|
||||||
6
package/secubox/secubox-app-mmpm/files/etc/config/mmpm
Normal file
6
package/secubox/secubox-app-mmpm/files/etc/config/mmpm
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# MMPM - MagicMirror Package Manager configuration
|
||||||
|
|
||||||
|
config mmpm 'main'
|
||||||
|
option enabled '0'
|
||||||
|
option port '7891'
|
||||||
|
option address '0.0.0.0'
|
||||||
36
package/secubox/secubox-app-mmpm/files/etc/init.d/mmpm
Normal file
36
package/secubox/secubox-app-mmpm/files/etc/init.d/mmpm
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/sh /etc/rc.common
|
||||||
|
# MMPM - MagicMirror Package Manager service
|
||||||
|
|
||||||
|
START=96
|
||||||
|
STOP=09
|
||||||
|
USE_PROCD=1
|
||||||
|
|
||||||
|
PROG=/usr/sbin/mmpmctl
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
local enabled
|
||||||
|
config_load mmpm
|
||||||
|
config_get enabled main enabled '0'
|
||||||
|
|
||||||
|
[ "$enabled" = "1" ] || return 0
|
||||||
|
|
||||||
|
procd_open_instance
|
||||||
|
procd_set_param command "$PROG" service-run
|
||||||
|
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
|
||||||
|
procd_set_param stdout 1
|
||||||
|
procd_set_param stderr 1
|
||||||
|
procd_close_instance
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
"$PROG" service-stop
|
||||||
|
}
|
||||||
|
|
||||||
|
reload_service() {
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
service_triggers() {
|
||||||
|
procd_add_reload_trigger "mmpm"
|
||||||
|
}
|
||||||
307
package/secubox/secubox-app-mmpm/files/usr/sbin/mmpmctl
Normal file
307
package/secubox/secubox-app-mmpm/files/usr/sbin/mmpmctl
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# MMPM - MagicMirror Package Manager controller
|
||||||
|
# Manages MMPM inside MagicMirror2 LXC container
|
||||||
|
# Copyright (C) 2024-2026 CyberMind.fr
|
||||||
|
|
||||||
|
CONFIG="mmpm"
|
||||||
|
LXC_NAME="magicmirror2"
|
||||||
|
MMPM_REPO="https://github.com/Bee-Mar/mmpm.git"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: mmpmctl <command> [options]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
install Install MMPM in MagicMirror2 container
|
||||||
|
update Update MMPM to latest version
|
||||||
|
status Show MMPM status
|
||||||
|
logs Show MMPM logs (use -f to follow)
|
||||||
|
shell Open shell in container (MMPM context)
|
||||||
|
service-run Internal: run MMPM GUI under procd
|
||||||
|
service-stop Stop MMPM GUI
|
||||||
|
|
||||||
|
Module Management (via MMPM):
|
||||||
|
search <query> Search for modules
|
||||||
|
list List installed modules
|
||||||
|
install <module> Install a module
|
||||||
|
remove <module> Remove a module
|
||||||
|
upgrade [module] Upgrade module(s)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
mmpmctl install
|
||||||
|
mmpmctl search weather
|
||||||
|
mmpmctl install MMM-WeatherChart
|
||||||
|
mmpmctl list
|
||||||
|
|
||||||
|
MMPM Web GUI: http://<router-ip>:7891
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info() { echo "[INFO] $*"; }
|
||||||
|
log_warn() { echo "[WARN] $*" >&2; }
|
||||||
|
log_error() { echo "[ERROR] $*" >&2; }
|
||||||
|
|
||||||
|
uci_get() { uci -q get ${CONFIG}.$1; }
|
||||||
|
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
|
||||||
|
|
||||||
|
load_config() {
|
||||||
|
port="$(uci_get main.port || echo 7891)"
|
||||||
|
address="$(uci_get main.address || echo 0.0.0.0)"
|
||||||
|
enabled="$(uci_get main.enabled || echo 0)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if MagicMirror2 LXC is running
|
||||||
|
check_mm2_running() {
|
||||||
|
if ! lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
log_error "MagicMirror2 container not running"
|
||||||
|
log_error "Start it first: /etc/init.d/magicmirror2 start"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if MMPM is installed in container
|
||||||
|
is_mmpm_installed() {
|
||||||
|
lxc-attach -n "$LXC_NAME" -- sh -c "command -v mmpm >/dev/null 2>&1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install MMPM in container
|
||||||
|
cmd_install() {
|
||||||
|
check_mm2_running || return 1
|
||||||
|
|
||||||
|
if is_mmpm_installed; then
|
||||||
|
log_info "MMPM is already installed"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Installing MMPM in MagicMirror2 container..."
|
||||||
|
|
||||||
|
# Install dependencies and MMPM via pip
|
||||||
|
lxc-attach -n "$LXC_NAME" -- sh -c '
|
||||||
|
# Update package lists
|
||||||
|
apt-get update 2>/dev/null || apk update 2>/dev/null || true
|
||||||
|
|
||||||
|
# Install Python and pip if not present
|
||||||
|
if ! command -v pip3 >/dev/null 2>&1; then
|
||||||
|
apt-get install -y python3 python3-pip 2>/dev/null || \
|
||||||
|
apk add python3 py3-pip 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install MMPM via pip
|
||||||
|
pip3 install --upgrade mmpm || pip install --upgrade mmpm
|
||||||
|
|
||||||
|
# Initialize MMPM database
|
||||||
|
mmpm db --yes 2>/dev/null || true
|
||||||
|
' || {
|
||||||
|
log_error "Failed to install MMPM"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info "MMPM installed successfully"
|
||||||
|
log_info "Enable and start the GUI: /etc/init.d/mmpm enable && /etc/init.d/mmpm start"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update MMPM
|
||||||
|
cmd_update() {
|
||||||
|
check_mm2_running || return 1
|
||||||
|
|
||||||
|
if ! is_mmpm_installed; then
|
||||||
|
log_error "MMPM not installed. Run 'mmpmctl install' first"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Updating MMPM..."
|
||||||
|
lxc-attach -n "$LXC_NAME" -- pip3 install --upgrade mmpm || {
|
||||||
|
log_error "Failed to update MMPM"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info "MMPM updated successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show status
|
||||||
|
cmd_status() {
|
||||||
|
load_config
|
||||||
|
|
||||||
|
echo "=== MMPM Status ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if ! lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then
|
||||||
|
echo "MagicMirror2 container: NOT RUNNING"
|
||||||
|
echo "MMPM: N/A (container not running)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "MagicMirror2 container: RUNNING"
|
||||||
|
|
||||||
|
if is_mmpm_installed; then
|
||||||
|
local version=$(lxc-attach -n "$LXC_NAME" -- mmpm --version 2>/dev/null || echo "unknown")
|
||||||
|
echo "MMPM installed: YES (v$version)"
|
||||||
|
else
|
||||||
|
echo "MMPM installed: NO"
|
||||||
|
echo ""
|
||||||
|
echo "Install with: mmpmctl install"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Configuration ==="
|
||||||
|
echo "GUI Port: $port"
|
||||||
|
echo "GUI Address: $address"
|
||||||
|
echo "GUI Enabled: $enabled"
|
||||||
|
|
||||||
|
# Check if GUI is running
|
||||||
|
if pgrep -f "mmpm gui" >/dev/null 2>&1; then
|
||||||
|
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||||
|
echo ""
|
||||||
|
echo "GUI Status: RUNNING"
|
||||||
|
echo "GUI URL: http://$router_ip:$port"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "GUI Status: NOT RUNNING"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show logs
|
||||||
|
cmd_logs() {
|
||||||
|
if [ "$1" = "-f" ]; then
|
||||||
|
logread -f -e mmpm
|
||||||
|
else
|
||||||
|
logread -e mmpm | tail -100
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run MMPM GUI (called by procd)
|
||||||
|
cmd_service_run() {
|
||||||
|
check_mm2_running || return 1
|
||||||
|
|
||||||
|
if ! is_mmpm_installed; then
|
||||||
|
log_error "MMPM not installed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
load_config
|
||||||
|
|
||||||
|
log_info "Starting MMPM GUI on port $port..."
|
||||||
|
|
||||||
|
# Run MMPM GUI inside container
|
||||||
|
exec lxc-attach -n "$LXC_NAME" -- mmpm gui --port "$port" --host "$address"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop MMPM GUI
|
||||||
|
cmd_service_stop() {
|
||||||
|
# Kill mmpm gui process in container
|
||||||
|
lxc-attach -n "$LXC_NAME" -- pkill -f "mmpm gui" 2>/dev/null || true
|
||||||
|
log_info "MMPM GUI stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Search modules
|
||||||
|
cmd_search() {
|
||||||
|
local query="$1"
|
||||||
|
check_mm2_running || return 1
|
||||||
|
|
||||||
|
if ! is_mmpm_installed; then
|
||||||
|
log_error "MMPM not installed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$query" ]; then
|
||||||
|
log_error "Search query required"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
lxc-attach -n "$LXC_NAME" -- mmpm search "$query"
|
||||||
|
}
|
||||||
|
|
||||||
|
# List installed modules
|
||||||
|
cmd_list() {
|
||||||
|
check_mm2_running || return 1
|
||||||
|
|
||||||
|
if ! is_mmpm_installed; then
|
||||||
|
log_error "MMPM not installed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
lxc-attach -n "$LXC_NAME" -- mmpm list
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install module via MMPM
|
||||||
|
cmd_module_install() {
|
||||||
|
local module="$1"
|
||||||
|
check_mm2_running || return 1
|
||||||
|
|
||||||
|
if ! is_mmpm_installed; then
|
||||||
|
log_error "MMPM not installed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$module" ]; then
|
||||||
|
log_error "Module name required"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Installing module: $module"
|
||||||
|
lxc-attach -n "$LXC_NAME" -- mmpm install "$module" --yes
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove module via MMPM
|
||||||
|
cmd_module_remove() {
|
||||||
|
local module="$1"
|
||||||
|
check_mm2_running || return 1
|
||||||
|
|
||||||
|
if ! is_mmpm_installed; then
|
||||||
|
log_error "MMPM not installed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$module" ]; then
|
||||||
|
log_error "Module name required"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Removing module: $module"
|
||||||
|
lxc-attach -n "$LXC_NAME" -- mmpm remove "$module" --yes
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upgrade modules
|
||||||
|
cmd_upgrade() {
|
||||||
|
local module="$1"
|
||||||
|
check_mm2_running || return 1
|
||||||
|
|
||||||
|
if ! is_mmpm_installed; then
|
||||||
|
log_error "MMPM not installed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$module" ]; then
|
||||||
|
log_info "Upgrading module: $module"
|
||||||
|
lxc-attach -n "$LXC_NAME" -- mmpm upgrade "$module" --yes
|
||||||
|
else
|
||||||
|
log_info "Upgrading all modules..."
|
||||||
|
lxc-attach -n "$LXC_NAME" -- mmpm upgrade --yes
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Open shell in container
|
||||||
|
cmd_shell() {
|
||||||
|
check_mm2_running || return 1
|
||||||
|
lxc-attach -n "$LXC_NAME" -- /bin/sh
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main entry point
|
||||||
|
case "${1:-}" in
|
||||||
|
install) shift; cmd_install "$@" ;;
|
||||||
|
update) shift; cmd_update "$@" ;;
|
||||||
|
status) shift; cmd_status "$@" ;;
|
||||||
|
logs) shift; cmd_logs "$@" ;;
|
||||||
|
shell) shift; cmd_shell "$@" ;;
|
||||||
|
service-run) shift; cmd_service_run "$@" ;;
|
||||||
|
service-stop) shift; cmd_service_stop "$@" ;;
|
||||||
|
search) shift; cmd_search "$@" ;;
|
||||||
|
list) shift; cmd_list "$@" ;;
|
||||||
|
install-module) shift; cmd_module_install "$@" ;;
|
||||||
|
remove) shift; cmd_module_remove "$@" ;;
|
||||||
|
upgrade) shift; cmd_upgrade "$@" ;;
|
||||||
|
help|--help|-h|'') usage ;;
|
||||||
|
*) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
Loading…
Reference in New Issue
Block a user