diff --git a/package/secubox/luci-app-magicmirror2/Makefile b/package/secubox/luci-app-magicmirror2/Makefile new file mode 100644 index 00000000..c32b42cd --- /dev/null +++ b/package/secubox/luci-app-magicmirror2/Makefile @@ -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 + +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)) diff --git a/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/magicmirror2/api.js b/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/magicmirror2/api.js new file mode 100644 index 00000000..5ec8fa2a --- /dev/null +++ b/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/magicmirror2/api.js @@ -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)); + } +}); diff --git a/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/view/magicmirror2/dashboard.js b/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/view/magicmirror2/dashboard.js new file mode 100644 index 00000000..4a362419 --- /dev/null +++ b/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/view/magicmirror2/dashboard.js @@ -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 +}); diff --git a/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/view/magicmirror2/modules.js b/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/view/magicmirror2/modules.js new file mode 100644 index 00000000..22c124bd --- /dev/null +++ b/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/view/magicmirror2/modules.js @@ -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 +}); diff --git a/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/view/magicmirror2/settings.js b/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/view/magicmirror2/settings.js new file mode 100644 index 00000000..16227d08 --- /dev/null +++ b/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/view/magicmirror2/settings.js @@ -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'); + }); + } +}); diff --git a/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/view/magicmirror2/webui.js b/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/view/magicmirror2/webui.js new file mode 100644 index 00000000..201a9b15 --- /dev/null +++ b/package/secubox/luci-app-magicmirror2/htdocs/luci-static/resources/view/magicmirror2/webui.js @@ -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 +}); diff --git a/package/secubox/luci-app-magicmirror2/root/usr/libexec/rpcd/luci.magicmirror2 b/package/secubox/luci-app-magicmirror2/root/usr/libexec/rpcd/luci.magicmirror2 new file mode 100644 index 00000000..6f307ef2 --- /dev/null +++ b/package/secubox/luci-app-magicmirror2/root/usr/libexec/rpcd/luci.magicmirror2 @@ -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 </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 </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 </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 </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 diff --git a/package/secubox/luci-app-magicmirror2/root/usr/share/luci/menu.d/luci-app-magicmirror2.json b/package/secubox/luci-app-magicmirror2/root/usr/share/luci/menu.d/luci-app-magicmirror2.json new file mode 100644 index 00000000..96c586d9 --- /dev/null +++ b/package/secubox/luci-app-magicmirror2/root/usr/share/luci/menu.d/luci-app-magicmirror2.json @@ -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" + } + } +} diff --git a/package/secubox/luci-app-magicmirror2/root/usr/share/rpcd/acl.d/luci-app-magicmirror2.json b/package/secubox/luci-app-magicmirror2/root/usr/share/rpcd/acl.d/luci-app-magicmirror2.json new file mode 100644 index 00000000..7e36b44b --- /dev/null +++ b/package/secubox/luci-app-magicmirror2/root/usr/share/rpcd/acl.d/luci-app-magicmirror2.json @@ -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"] + } + } +} diff --git a/package/secubox/luci-app-mmpm/Makefile b/package/secubox/luci-app-mmpm/Makefile new file mode 100644 index 00000000..bd7f0afa --- /dev/null +++ b/package/secubox/luci-app-mmpm/Makefile @@ -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 + +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)) diff --git a/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/mmpm/api.js b/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/mmpm/api.js new file mode 100644 index 00000000..8d61efd1 --- /dev/null +++ b/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/mmpm/api.js @@ -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)); + } +}); diff --git a/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/view/mmpm/dashboard.js b/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/view/mmpm/dashboard.js new file mode 100644 index 00000000..ebe88170 --- /dev/null +++ b/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/view/mmpm/dashboard.js @@ -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; + } +}); diff --git a/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/view/mmpm/modules.js b/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/view/mmpm/modules.js new file mode 100644 index 00000000..e370890b --- /dev/null +++ b/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/view/mmpm/modules.js @@ -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; + } +}); diff --git a/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/view/mmpm/settings.js b/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/view/mmpm/settings.js new file mode 100644 index 00000000..49eef478 --- /dev/null +++ b/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/view/mmpm/settings.js @@ -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(); + } +}); diff --git a/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/view/mmpm/webui.js b/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/view/mmpm/webui.js new file mode 100644 index 00000000..88b610ab --- /dev/null +++ b/package/secubox/luci-app-mmpm/htdocs/luci-static/resources/view/mmpm/webui.js @@ -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; + } +}); diff --git a/package/secubox/luci-app-mmpm/root/usr/libexec/rpcd/luci.mmpm b/package/secubox/luci-app-mmpm/root/usr/libexec/rpcd/luci.mmpm new file mode 100644 index 00000000..64fbcc38 --- /dev/null +++ b/package/secubox/luci-app-mmpm/root/usr/libexec/rpcd/luci.mmpm @@ -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 <&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 </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 diff --git a/package/secubox/luci-app-mmpm/root/usr/share/luci/menu.d/luci-app-mmpm.json b/package/secubox/luci-app-mmpm/root/usr/share/luci/menu.d/luci-app-mmpm.json new file mode 100644 index 00000000..20350b6d --- /dev/null +++ b/package/secubox/luci-app-mmpm/root/usr/share/luci/menu.d/luci-app-mmpm.json @@ -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" + } + } +} diff --git a/package/secubox/luci-app-mmpm/root/usr/share/rpcd/acl.d/luci-app-mmpm.json b/package/secubox/luci-app-mmpm/root/usr/share/rpcd/acl.d/luci-app-mmpm.json new file mode 100644 index 00000000..6e43eaa1 --- /dev/null +++ b/package/secubox/luci-app-mmpm/root/usr/share/rpcd/acl.d/luci-app-mmpm.json @@ -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"] + } + } +} diff --git a/package/secubox/luci-app-secubox-portal/Makefile b/package/secubox/luci-app-secubox-portal/Makefile index e03a391d..ed5717d4 100644 --- a/package/secubox/luci-app-secubox-portal/Makefile +++ b/package/secubox/luci-app-secubox-portal/Makefile @@ -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 diff --git a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/portal.js b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/portal.js index 29e8890e..76c0ad33 100644 --- a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/portal.js +++ b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox-portal/portal.js @@ -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' } }, diff --git a/package/secubox/secubox-app-magicmirror2/Makefile b/package/secubox/secubox-app-magicmirror2/Makefile new file mode 100644 index 00000000..3ce69f49 --- /dev/null +++ b/package/secubox/secubox-app-magicmirror2/Makefile @@ -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 +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://:8082" + echo "" + echo "To manage modules:" + echo " mm2ctl module list" + echo " mm2ctl module install MMM-" + echo "" +} +exit 0 +endef + +$(eval $(call BuildPackage,secubox-app-magicmirror2)) diff --git a/package/secubox/secubox-app-magicmirror2/files/etc/config/magicmirror2 b/package/secubox/secubox-app-magicmirror2/files/etc/config/magicmirror2 new file mode 100644 index 00000000..e42cdf9b --- /dev/null +++ b/package/secubox/secubox-app-magicmirror2/files/etc/config/magicmirror2 @@ -0,0 +1,48 @@ +# MagicMirror2 configuration for SecuBox + +config magicmirror2 'main' + option enabled '0' + option port '8082' + option address '0.0.0.0' + option data_path '/srv/magicmirror2' + option memory_limit '512M' + option language 'en' + option timezone 'Europe/Paris' + option units 'metric' + option electron_enabled '0' + +config display 'display' + option width '1920' + option height '1080' + option zoom '1.0' + option brightness '100' + +config weather 'weather' + option enabled '0' + option provider 'openweathermap' + option api_key '' + option location '' + option location_id '' + option units 'metric' + +config calendar 'calendar' + option enabled '0' + option max_entries '10' + option fetch_interval '300000' + +config newsfeed 'newsfeed' + option enabled '0' + option show_description '1' + option show_source_title '1' + option max_news_items '5' + +config clock 'clock' + option enabled '1' + option display_seconds '1' + option show_date '1' + option show_week '0' + option date_format 'dddd, LL' + +config compliments 'compliments' + option enabled '1' + option update_interval '30000' diff --git a/package/secubox/secubox-app-magicmirror2/files/etc/init.d/magicmirror2 b/package/secubox/secubox-app-magicmirror2/files/etc/init.d/magicmirror2 new file mode 100644 index 00000000..f490b470 --- /dev/null +++ b/package/secubox/secubox-app-magicmirror2/files/etc/init.d/magicmirror2 @@ -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" +} diff --git a/package/secubox/secubox-app-magicmirror2/files/usr/sbin/mm2ctl b/package/secubox/secubox-app-magicmirror2/files/usr/sbin/mm2ctl new file mode 100644 index 00000000..c260383d --- /dev/null +++ b/package/secubox/secubox-app-magicmirror2/files/usr/sbin/mm2ctl @@ -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 [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 Install a module (MMM-name or git URL) + module remove Remove an installed module + module update [name] Update module(s) + +Configuration: + set Set UCI configuration value + get 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://: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 ' 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://:$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 " + 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 " + 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 diff --git a/package/secubox/secubox-app-mmpm/Makefile b/package/secubox/secubox-app-mmpm/Makefile new file mode 100644 index 00000000..99cd8ffd --- /dev/null +++ b/package/secubox/secubox-app-mmpm/Makefile @@ -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 +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://:7891" + echo "" +} +exit 0 +endef + +$(eval $(call BuildPackage,secubox-app-mmpm)) diff --git a/package/secubox/secubox-app-mmpm/files/etc/config/mmpm b/package/secubox/secubox-app-mmpm/files/etc/config/mmpm new file mode 100644 index 00000000..23b794d0 --- /dev/null +++ b/package/secubox/secubox-app-mmpm/files/etc/config/mmpm @@ -0,0 +1,6 @@ +# MMPM - MagicMirror Package Manager configuration + +config mmpm 'main' + option enabled '0' + option port '7891' + option address '0.0.0.0' diff --git a/package/secubox/secubox-app-mmpm/files/etc/init.d/mmpm b/package/secubox/secubox-app-mmpm/files/etc/init.d/mmpm new file mode 100644 index 00000000..056be0ac --- /dev/null +++ b/package/secubox/secubox-app-mmpm/files/etc/init.d/mmpm @@ -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" +} diff --git a/package/secubox/secubox-app-mmpm/files/usr/sbin/mmpmctl b/package/secubox/secubox-app-mmpm/files/usr/sbin/mmpmctl new file mode 100644 index 00000000..abd72114 --- /dev/null +++ b/package/secubox/secubox-app-mmpm/files/usr/sbin/mmpmctl @@ -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 [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 Search for modules + list List installed modules + install Install a module + remove Remove a module + upgrade [module] Upgrade module(s) + +Examples: + mmpmctl install + mmpmctl search weather + mmpmctl install MMM-WeatherChart + mmpmctl list + +MMPM Web GUI: http://: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