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:
CyberMind-FR 2026-01-17 12:00:18 +01:00
parent 447e4ab2be
commit a83cde0885
28 changed files with 4306 additions and 2 deletions

View 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))

View File

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

View File

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

View File

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

View File

@ -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');
});
}
});

View File

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

View File

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

View File

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

View File

@ -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"]
}
}
}

View 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))

View File

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

View File

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

View File

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

View File

@ -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();
}
});

View File

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

View 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

View File

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

View File

@ -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"]
}
}
}

View File

@ -10,8 +10,8 @@ LUCI_TITLE:=SecuBox Portal - Unified WebUI
LUCI_DESCRIPTION:=Unified entry point for all SecuBox applications with tabbed navigation
LUCI_DEPENDS:=+luci-base +luci-theme-secubox
LUCI_PKGARCH:=all
PKG_VERSION:=1.0.2
PKG_RELEASE:=2
PKG_VERSION:=0.6.0
PKG_RELEASE:=1
PKG_LICENSE:=GPL-3.0-or-later
PKG_MAINTAINER:=SecuBox Team <secubox@example.com>

View File

@ -211,6 +211,30 @@ return baseclass.extend({
path: 'admin/secubox/services/vhosts/overview',
service: 'nginx',
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'
}
},

View 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))

View File

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

View File

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

View 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

View 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))

View 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'

View 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"
}

View 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