This commit is contained in:
CyberMind-FR 2026-01-23 05:44:38 +01:00
parent d80501b33a
commit 3132ef9c14
20 changed files with 241 additions and 1657 deletions

View File

@ -107,7 +107,17 @@
"WebFetch(domain:hub.docker.com)",
"WebFetch(domain:localai.io)",
"WebFetch(domain:downloads.lms-community.org)",
"Bash(./secubox-tools/sdk/build-package.sh:*)"
"Bash(./secubox-tools/sdk/build-package.sh:*)",
"Bash(./secubox-tools/scripts/expand-openwrt-image.sh:*)",
"Bash(parted:*)",
"Bash(fdisk:*)",
"Bash(sudo ./secubox-tools/scripts/expand-openwrt-image.sh:*)",
"Bash(gunzip:*)",
"Bash(xxd:*)",
"Bash(sfdisk:*)",
"Bash(xzcat:*)",
"Bash(head:*)",
"Bash(docker search:*)"
]
}
}

View File

@ -1,23 +0,0 @@
# Copyright (C) 2024 CyberMind.fr
# Licensed under Apache-2.0
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-magicmirror
PKG_VERSION:=1.0.0
PKG_RELEASE:=2
PKG_ARCH:=all
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
LUCI_TITLE:=MagicMirror² Manager
LUCI_DESCRIPTION:=Web-based module manager and controller for MagicMirror² smart mirror platform
LUCI_DEPENDS:=+luci-base +rpcd +secubox-app-magicmirror +jq
LUCI_PKGARCH:=all
# File permissions
PKG_FILE_MODES:=/usr/libexec/rpcd/luci.magicmirror:root:root:755
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -1,212 +0,0 @@
'use strict';
'require view';
'require rpc';
'require ui';
'require dom';
var callGetConfig = rpc.declare({
object: 'luci.magicmirror',
method: 'getConfig',
expect: { }
});
var callSaveConfig = rpc.declare({
object: 'luci.magicmirror',
method: 'saveConfig',
params: ['content'],
expect: { }
});
var callRestartService = rpc.declare({
object: 'luci.magicmirror',
method: 'restartService',
expect: { }
});
return view.extend({
load: function() {
return Promise.all([
callGetConfig()
]);
},
render: function(data) {
var configData = data[0] || {};
var configContent = configData.content || '';
var textarea;
var saveConfig = function() {
var content = textarea.value;
if (!content.trim()) {
ui.addNotification(null, E('p', _('Configuration cannot be empty')), 'warning');
return;
}
// Basic validation
if (!content.includes('let config')) {
if (!confirm(_('Configuration does not contain "let config". Are you sure you want to save this?'))) {
return;
}
}
ui.showModal(_('Saving Configuration'), [
E('p', { 'class': 'spinning' }, _('Saving configuration...'))
]);
return callSaveConfig(content).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Configuration saved successfully')), 'info');
if (confirm(_('Configuration saved. Restart MagicMirror to apply changes?'))) {
ui.showModal(_('Restarting Service'), [
E('p', { 'class': 'spinning' }, _('Restarting MagicMirror²...'))
]);
return callRestartService().then(function() {
ui.hideModal();
ui.addNotification(null, E('p', _('Service restarted. Changes will apply shortly.')), 'info');
});
}
} else {
ui.addNotification(null, E('p', _('Failed to save configuration: ') + (result.error || 'Unknown error')), 'error');
}
});
};
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('MagicMirror² Configuration')),
E('div', { 'class': 'cbi-section-descr' }, _('Edit the MagicMirror² configuration file (config.js)')),
E('div', { 'class': 'cbi-section', 'style': 'margin-top: 20px;' }, [
E('div', { 'class': 'cbi-section-descr', 'style': 'margin-bottom: 15px;' }, [
E('div', { 'class': 'alert-message info' }, [
E('h4', { 'style': 'margin: 0 0 10px 0;' }, _('Configuration Tips:')),
E('ul', { 'style': 'margin: 0; padding-left: 20px;' }, [
E('li', {}, _('Modules are configured in the "modules" array')),
E('li', {}, _('Each module has a "module" name, "position", and optional "config" object')),
E('li', {}, _('Common positions: top_left, top_right, bottom_bar, etc.')),
E('li', {}, _('Save and restart the service to apply changes')),
E('li', {}, _('Always backup your config before making major changes'))
])
])
]),
E('div', { 'class': 'cbi-value', 'style': 'margin-bottom: 15px;' }, [
E('label', { 'class': 'cbi-value-title' }, _('Configuration')),
E('div', { 'class': 'cbi-value-field' }, [
textarea = E('textarea', {
'id': 'config-editor',
'style': 'width: 100%; min-height: 600px; font-family: monospace; font-size: 13px; line-height: 1.5; padding: 10px; border: 1px solid #ccc; border-radius: 3px;',
'spellcheck': 'false'
}, configContent)
])
]),
E('div', { 'class': 'cbi-page-actions' }, [
E('button', {
'class': 'cbi-button cbi-button-save',
'click': saveConfig
}, _('Save Configuration')),
' ',
E('button', {
'class': 'cbi-button cbi-button-neutral',
'click': function() {
if (confirm(_('Discard all changes and reload?'))) {
window.location.reload();
}
}
}, _('Cancel'))
])
]),
E('div', { 'class': 'cbi-section', 'style': 'margin-top: 30px;' }, [
E('h3', {}, _('Module Position Reference')),
E('div', { 'style': 'background: #f5f5f5; padding: 20px; border-radius: 5px; font-family: monospace; font-size: 12px;' }, [
E('pre', { 'style': 'margin: 0; overflow-x: auto;' },
`╔══════════════════════════════════════════════════╗
top_bar
top_left top_center top_right
upper_third middle_center upper_third
lower_third lower_third
bottom_left bottom_center bottom_right
bottom_bar
`
)
])
]),
E('div', { 'class': 'cbi-section', 'style': 'margin-top: 20px;' }, [
E('h3', {}, _('Example Module Configuration')),
E('pre', { 'style': 'background: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto; font-size: 12px;' },
`{
module: "weather",
position: "top_right",
config: {
weatherProvider: "openweathermap",
type: "current",
location: "Paris",
locationID: "2988507",
apiKey: "YOUR_API_KEY"
}
},
{
module: "calendar",
header: "My Calendar",
position: "top_left",
config: {
calendars: [
{
symbol: "calendar-check",
url: "webcal://calendar.google.com/calendar/ical/..."
}
]
}
},
{
module: "newsfeed",
position: "bottom_bar",
config: {
feeds: [
{
title: "BBC News",
url: "http://feeds.bbci.co.uk/news/rss.xml"
}
],
showSourceTitle: true,
showPublishDate: true
}
}`
)
]),
E('div', { 'class': 'cbi-section', 'style': 'margin-top: 20px;' }, [
E('h3', {}, _('Resources')),
E('ul', {}, [
E('li', {}, [E('a', { 'href': 'https://docs.magicmirror.builders/configuration/introduction.html', 'target': '_blank' }, _('Configuration Documentation'))]),
E('li', {}, [E('a', { 'href': 'https://docs.magicmirror.builders/modules/configuration.html', 'target': '_blank' }, _('Module Configuration'))]),
E('li', {}, [E('a', { 'href': 'https://github.com/MichMich/MagicMirror/tree/master/modules/default', 'target': '_blank' }, _('Default Modules'))]),
E('li', {}, [E('a', { 'href': 'https://github.com/MichMich/MagicMirror/wiki/3rd-party-modules', 'target': '_blank' }, _('3rd Party Modules'))])
])
])
]);
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});

View File

@ -1,282 +0,0 @@
'use strict';
'require view';
'require rpc';
'require ui';
'require dom';
'require poll';
var callListModules = rpc.declare({
object: 'luci.magicmirror',
method: 'listModules',
expect: { }
});
var callInstallModule = rpc.declare({
object: 'luci.magicmirror',
method: 'installModule',
params: ['url'],
expect: { }
});
var callRemoveModule = rpc.declare({
object: 'luci.magicmirror',
method: 'removeModule',
params: ['name'],
expect: { }
});
var callUpdateModule = rpc.declare({
object: 'luci.magicmirror',
method: 'updateModule',
params: ['name'],
expect: { }
});
var callGetModuleConfig = rpc.declare({
object: 'luci.magicmirror',
method: 'getModuleConfig',
params: ['name'],
expect: { }
});
return view.extend({
load: function() {
return Promise.all([
callListModules()
]);
},
renderModuleCard: function(module) {
return E('div', {
'class': 'cbi-section',
'style': 'margin-bottom: 15px; border: 1px solid #ddd; border-radius: 5px; padding: 15px;'
}, [
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: start;' }, [
E('div', { 'style': 'flex: 1;' }, [
E('h3', { 'style': 'margin: 0 0 10px 0; color: #2196F3;' }, module.name),
module.description ? E('p', { 'style': 'margin: 0 0 5px 0; color: #666;' }, module.description) : '',
E('div', { 'style': 'font-size: 12px; color: #999;' }, [
module.author ? E('span', {}, _('Author: ') + module.author + ' | ') : '',
module.version ? E('span', {}, _('Version: ') + module.version) : ''
])
]),
E('div', { 'style': 'display: flex; gap: 5px; flex-wrap: wrap;' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, function() {
return this.showModuleConfig(module.name);
})
}, _('Info')),
E('button', {
'class': 'cbi-button cbi-button-apply',
'click': ui.createHandlerFn(this, function() {
return this.updateModule(module.name);
})
}, _('Update')),
E('button', {
'class': 'cbi-button cbi-button-negative',
'click': ui.createHandlerFn(this, function() {
return this.removeModule(module.name);
})
}, _('Remove'))
])
])
]);
},
showModuleConfig: function(moduleName) {
ui.showModal(_('Module Information: ') + moduleName, [
E('p', { 'class': 'spinning' }, _('Loading...'))
]);
return callGetModuleConfig(moduleName).then(function(result) {
if (result.success) {
var content = result.readme || 'No information available.';
// Truncate very long READMEs
if (content.length > 5000) {
content = content.substring(0, 5000) + '\n\n... (truncated)';
}
ui.showModal(_('Module Information: ') + moduleName, [
E('div', { 'style': 'max-height: 500px; overflow-y: auto;' }, [
E('pre', { 'style': 'white-space: pre-wrap; font-size: 12px;' }, content)
]),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button',
'click': ui.hideModal
}, _('Close'))
])
], 'cbi-modal');
} else {
ui.hideModal();
ui.addNotification(null, E('p', _('Failed to load module information')), 'error');
}
});
},
updateModule: function(moduleName) {
if (!confirm(_('Update module ') + moduleName + '?')) {
return;
}
ui.showModal(_('Updating Module'), [
E('p', { 'class': 'spinning' }, _('Updating ') + moduleName + '...')
]);
return callUpdateModule(moduleName).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Module update started. This may take a minute...')), 'info');
setTimeout(function() { window.location.reload(); }, 5000);
} else {
ui.addNotification(null, E('p', _('Failed to update module: ') + (result.error || 'Unknown error')), 'error');
}
});
},
removeModule: function(moduleName) {
if (!confirm(_('Remove module ') + moduleName + '? This cannot be undone.')) {
return;
}
ui.showModal(_('Removing Module'), [
E('p', { 'class': 'spinning' }, _('Removing ') + moduleName + '...')
]);
return callRemoveModule(moduleName).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Module removed successfully')), 'info');
setTimeout(function() { window.location.reload(); }, 1500);
} else {
ui.addNotification(null, E('p', _('Failed to remove module: ') + (result.error || 'Unknown error')), 'error');
}
});
},
showInstallDialog: function() {
var input;
ui.showModal(_('Install MagicMirror Module'), [
E('p', {}, _('Enter the Git repository URL of the module to install:')),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Git URL')),
E('div', { 'class': 'cbi-value-field' }, [
input = E('input', {
'type': 'text',
'class': 'cbi-input-text',
'placeholder': 'https://github.com/user/MMM-ModuleName',
'style': 'width: 100%;'
})
])
]),
E('div', { 'style': 'margin-top: 15px;' }, [
E('p', { 'style': 'font-size: 12px; color: #666;' }, [
E('strong', {}, _('Examples:')),
E('br'), '• https://github.com/MichMich/MMM-WeatherChart',
E('br'), '• https://github.com/hangorazvan/covid19',
E('br'), '• https://github.com/jclarke0000/MMM-MyCalendar'
])
]),
E('div', { 'class': 'right', 'style': 'margin-top: 20px;' }, [
E('button', {
'class': 'cbi-button cbi-button-neutral',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': ui.createHandlerFn(this, function() {
var url = input.value.trim();
if (!url) {
ui.addNotification(null, E('p', _('Please enter a Git URL')), 'warning');
return;
}
return this.installModule(url);
})
}, _('Install'))
])
], 'cbi-modal');
input.focus();
},
installModule: function(url) {
ui.hideModal();
ui.showModal(_('Installing Module'), [
E('p', { 'class': 'spinning' }, _('Installing module from ') + url + '...'),
E('p', { 'style': 'font-size: 12px; color: #666;' }, _('This may take a few minutes depending on the module size.'))
]);
return callInstallModule(url).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Module installation started. Page will reload in 10 seconds...')), 'info');
setTimeout(function() { window.location.reload(); }, 10000);
} else {
ui.addNotification(null, E('p', _('Failed to install module: ') + (result.error || 'Unknown error')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: ') + err.message), 'error');
});
},
render: function(data) {
var modulesData = data[0] || {};
var modules = (modulesData.modules || []);
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('MagicMirror² Modules')),
E('div', { 'class': 'cbi-section-descr' }, _('Manage installed modules and install new ones from the MagicMirror community')),
E('div', { 'class': 'cbi-section', 'style': 'margin: 20px 0;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;' }, [
E('h3', {}, _('Installed Modules')),
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': ui.createHandlerFn(this, this.showInstallDialog)
}, _('Install New Module'))
]),
modules.length > 0 ?
E('div', {}, modules.map(function(module) {
return this.renderModuleCard(module);
}, this)) :
E('div', { 'class': 'alert-message warning' }, [
E('p', {}, _('No modules installed yet.')),
E('p', {}, _('Click "Install New Module" to get started.'))
])
]),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Popular Modules')),
E('div', { 'style': 'columns: 2; column-gap: 20px;' }, [
E('ul', { 'style': 'margin: 0; padding-left: 20px;' }, [
E('li', {}, [E('a', { 'href': 'https://github.com/MichMich/MMM-WeatherChart', 'target': '_blank' }, 'MMM-WeatherChart'), ' - Weather charts']),
E('li', {}, [E('a', { 'href': 'https://github.com/jclarke0000/MMM-MyCalendar', 'target': '_blank' }, 'MMM-MyCalendar'), ' - Enhanced calendar']),
E('li', {}, [E('a', { 'href': 'https://github.com/cowboysdude/MMM-NOAA', 'target': '_blank' }, 'MMM-NOAA'), ' - NOAA weather']),
E('li', {}, [E('a', { 'href': 'https://github.com/hangorazvan/covid19', 'target': '_blank' }, 'MMM-COVID19'), ' - COVID-19 stats']),
E('li', {}, [E('a', { 'href': 'https://github.com/MichMich/MMM-Facial-Recognition', 'target': '_blank' }, 'MMM-Facial-Recognition'), ' - Face recognition']),
E('li', {}, [E('a', { 'href': 'https://github.com/cowboysdude/MMM-cryptocurrency', 'target': '_blank' }, 'MMM-Cryptocurrency'), ' - Crypto prices']),
E('li', {}, [E('a', { 'href': 'https://github.com/cowboysdude/MMM-BMW-DS', 'target': '_blank' }, 'MMM-BMW-DS'), ' - BMW status']),
E('li', {}, [E('a', { 'href': 'https://github.com/MichMich/MMM-Todoist', 'target': '_blank' }, 'MMM-Todoist'), ' - Todoist tasks']),
E('li', {}, [E('a', { 'href': 'https://github.com/MichMich/MMM-Spotify', 'target': '_blank' }, 'MMM-Spotify'), ' - Spotify player']),
E('li', {}, [E('a', { 'href': 'https://github.com/MichMich/MMM-GooglePhotos', 'target': '_blank' }, 'MMM-GooglePhotos'), ' - Google Photos'])
])
]),
E('p', { 'style': 'margin-top: 15px; font-size: 12px; color: #666;' }, [
_('Find more modules at: '),
E('a', { 'href': 'https://github.com/MichMich/MagicMirror/wiki/3rd-party-modules', 'target': '_blank' }, _('MagicMirror Wiki'))
])
])
]);
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});

View File

@ -1,180 +0,0 @@
'use strict';
'require view';
'require form';
'require rpc';
'require ui';
'require poll';
var callMagicMirrorStatus = rpc.declare({
object: 'luci.magicmirror',
method: 'getStatus',
expect: { }
});
var callMagicMirrorRestart = rpc.declare({
object: 'luci.magicmirror',
method: 'restartService',
expect: { }
});
return view.extend({
load: function() {
return Promise.all([
callMagicMirrorStatus()
]);
},
render: function(data) {
var status = data[0] || {};
var m, s, o;
m = new form.Map('magicmirror', _('MagicMirror² Overview'),
_('Smart mirror platform for displaying calendar, weather, news, and custom information modules'));
// Status section
s = m.section(form.NamedSection, '_status', 'status', _('Service Status'));
o = s.option(form.DummyValue, '_service_status', _('Status'));
o.rawhtml = true;
o.cfgvalue = function() {
var running = status.status && status.status.running;
var enabled = status.status && status.status.enabled;
var html = '<div style="display: flex; align-items: center; gap: 10px;">';
if (running) {
html += '<span style="color: #4CAF50; font-weight: bold;">● Running</span>';
} else {
html += '<span style="color: #f44336; font-weight: bold;">● Stopped</span>';
}
html += '<span style="color: #666;">|</span>';
html += '<span>Auto-start: ' + (enabled ? 'Enabled' : 'Disabled') + '</span>';
html += '</div>';
return html;
};
o = s.option(form.DummyValue, '_stats', _('Statistics'));
o.rawhtml = true;
o.cfgvalue = function() {
var stats = status.stats || {};
var port = (status.status && status.status.port) || '8080';
var html = '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin: 10px 0;">';
html += '<div style="background: #f0f0f0; padding: 15px; border-radius: 5px; text-align: center;">';
html += '<div style="font-size: 24px; font-weight: bold; color: #2196F3;">' + (stats.modules_installed || 0) + '</div>';
html += '<div style="color: #666; margin-top: 5px;">Modules Installed</div>';
html += '</div>';
html += '<div style="background: #f0f0f0; padding: 15px; border-radius: 5px; text-align: center;">';
html += '<div style="font-size: 18px; font-weight: bold; color: #FF9800;">' + port + '</div>';
html += '<div style="color: #666; margin-top: 5px;">Web Port</div>';
html += '</div>';
html += '</div>';
return html;
};
o = s.option(form.Button, '_open', _('Quick Access'));
o.inputtitle = _('Open Mirror');
o.inputstyle = 'apply';
o.onclick = function() {
var port = (status.status && status.status.port) || '8080';
window.open('http://' + window.location.hostname + ':' + port, '_blank');
};
o = s.option(form.Button, '_restart', _('Service Control'));
o.inputtitle = _('Restart Service');
o.inputstyle = 'action';
o.onclick = function() {
return ui.showModal(_('Restarting MagicMirror²'), [
E('p', { 'class': 'spinning' }, _('Please wait...'))
]) || Promise.resolve(callMagicMirrorRestart()).then(function() {
ui.hideModal();
ui.addNotification(null, E('p', _('Service restart initiated')), 'info');
setTimeout(function() { window.location.reload(); }, 3000);
});
};
// Configuration section
s = m.section(form.NamedSection, 'main', 'magicmirror', _('Basic Configuration'));
o = s.option(form.Flag, 'enabled', _('Enable MagicMirror²'),
_('Start MagicMirror² service on boot'));
o.default = '0';
o.rmempty = false;
o = s.option(form.Value, 'port', _('Web Port'),
_('Port for accessing the MagicMirror² web interface'));
o.default = '8080';
o.datatype = 'port';
o.placeholder = '8080';
o = s.option(form.Value, 'timezone', _('Timezone'),
_('Timezone for date/time display (e.g., Europe/Paris, America/New_York)'));
o.default = 'UTC';
o.placeholder = 'UTC';
o = s.option(form.ListValue, 'language', _('Language'),
_('Interface language'));
o.value('en', _('English'));
o.value('fr', _('French'));
o.value('de', _('German'));
o.value('es', _('Spanish'));
o.value('it', _('Italian'));
o.value('nl', _('Dutch'));
o.value('pl', _('Polish'));
o.value('pt', _('Portuguese'));
o.value('ru', _('Russian'));
o.value('zh-cn', _('Chinese (Simplified)'));
o.value('ja', _('Japanese'));
o.default = 'en';
o = s.option(form.ListValue, 'units', _('Units'),
_('Temperature and measurement units'));
o.value('metric', _('Metric (°C, km)'));
o.value('imperial', _('Imperial (°F, miles)'));
o.default = 'metric';
// Info section
s = m.section(form.NamedSection, '_info', 'info', _('Information'));
o = s.option(form.DummyValue, '_help');
o.rawhtml = true;
o.cfgvalue = function() {
return '<div class="cbi-value-description">' +
'<h4>About MagicMirror²:</h4>' +
'<p>MagicMirror² is an open source modular smart mirror platform. It displays information like calendar events, weather, news, and more on any display.</p>' +
'<h4>Getting Started:</h4>' +
'<ol>' +
'<li>Configure basic settings above</li>' +
'<li>Install modules from the <a href="/cgi-bin/luci/admin/secubox/iot/magicmirror/modules">Modules</a> page</li>' +
'<li>Customize the layout in <a href="/cgi-bin/luci/admin/secubox/iot/magicmirror/config">Configuration</a></li>' +
'<li>Access your mirror via the web interface or connect to a display</li>' +
'</ol>' +
'<h4>Module Positions:</h4>' +
'<ul>' +
'<li><code>top_bar</code> - Full width top bar</li>' +
'<li><code>top_left</code>, <code>top_center</code>, <code>top_right</code> - Top row</li>' +
'<li><code>upper_third</code>, <code>middle_center</code>, <code>lower_third</code> - Middle row</li>' +
'<li><code>bottom_left</code>, <code>bottom_center</code>, <code>bottom_right</code> - Bottom row</li>' +
'<li><code>bottom_bar</code>, <code>fullscreen_above</code>, <code>fullscreen_below</code> - Special positions</li>' +
'</ul>' +
'<h4>Resources:</h4>' +
'<ul>' +
'<li><a href="https://docs.magicmirror.builders/" target="_blank">Official Documentation</a></li>' +
'<li><a href="https://github.com/MichMich/MagicMirror/wiki/3rd-party-modules" target="_blank">3rd Party Modules</a></li>' +
'<li><a href="https://forum.magicmirror.builders/" target="_blank">Community Forum</a></li>' +
'</ul>' +
'</div>';
};
return m.render();
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});

View File

@ -1,308 +0,0 @@
#!/bin/sh
# RPCD backend for MagicMirror² Manager
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
CONFIG="magicmirror"
CONTAINER="secbx-magicmirror"
get_config_value() {
uci -q get ${CONFIG}.main.$1
}
get_modules_path() {
local path
path=$(get_config_value modules_path)
echo "${path:-/srv/magicmirror/modules}"
}
get_config_path() {
local path
path=$(get_config_value config_path)
echo "${path:-/srv/magicmirror/config}"
}
list_modules() {
local modules_path
modules_path=$(get_modules_path)
json_init
json_add_array "modules"
if [ -d "$modules_path" ]; then
for module_dir in "$modules_path"/MMM-*; do
if [ -d "$module_dir" ]; then
local module_name=$(basename "$module_dir")
local description=""
local author=""
local version=""
# Try to extract info from package.json
if [ -f "$module_dir/package.json" ]; then
description=$(jq -r '.description // ""' "$module_dir/package.json" 2>/dev/null)
author=$(jq -r '.author // ""' "$module_dir/package.json" 2>/dev/null)
version=$(jq -r '.version // ""' "$module_dir/package.json" 2>/dev/null)
fi
json_add_object
json_add_string "name" "$module_name"
json_add_string "description" "$description"
json_add_string "author" "$author"
json_add_string "version" "$version"
json_add_string "path" "$module_dir"
json_close_object
fi
done
fi
json_close_array
}
get_config_content() {
local config_path
config_path=$(get_config_path)
local config_file="$config_path/config.js"
if [ -f "$config_file" ]; then
cat "$config_file"
else
echo ""
fi
}
case "$1" in
list)
json_init
json_add_object "getStatus"
json_close_object
json_add_object "listModules"
json_close_object
json_add_object "getConfig"
json_close_object
json_add_object "installModule"
json_add_string "url" "string"
json_close_object
json_add_object "removeModule"
json_add_string "name" "string"
json_close_object
json_add_object "updateModule"
json_add_string "name" "string"
json_close_object
json_add_object "getModuleConfig"
json_add_string "name" "string"
json_close_object
json_add_object "saveConfig"
json_add_string "content" "string"
json_close_object
json_add_object "restartService"
json_close_object
json_dump
;;
call)
case "$2" in
getStatus)
# Get service status
json_init
json_add_boolean "success" 1
# Check if container is running
local running=0
docker ps --filter "name=$CONTAINER" --format '{{.Names}}' 2>/dev/null | grep -q "$CONTAINER" && running=1
# Get config values
config_load "$CONFIG"
local enabled port
config_get enabled main enabled "0"
config_get port main port "8080"
json_add_object "status"
json_add_boolean "running" "$running"
json_add_boolean "enabled" "$enabled"
json_add_string "port" "$port"
json_add_string "container" "$CONTAINER"
json_close_object
# Count modules
local modules_count=0
local modules_path
modules_path=$(get_modules_path)
if [ -d "$modules_path" ]; then
modules_count=$(find "$modules_path" -maxdepth 1 -name "MMM-*" -type d 2>/dev/null | wc -l)
fi
json_add_object "stats"
json_add_int "modules_installed" "$modules_count"
json_close_object
json_dump
;;
listModules)
json_init
json_add_boolean "success" 1
list_modules
json_dump
;;
getConfig)
json_init
json_add_boolean "success" 1
json_add_string "content" "$(get_config_content)"
json_dump
;;
installModule)
read input
json_load "$input"
local url
json_get_var url url
if [ -z "$url" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "URL is required"
json_dump
exit 0
fi
# Install module in background
/usr/sbin/magicmirrorctl module install "$url" >/tmp/mm-install.log 2>&1 &
json_init
json_add_boolean "success" 1
json_add_string "message" "Module installation started"
json_dump
;;
removeModule)
read input
json_load "$input"
local name
json_get_var name name
if [ -z "$name" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Module name is required"
json_dump
exit 0
fi
# Remove module
/usr/sbin/magicmirrorctl module remove "$name" >/dev/null 2>&1
json_init
json_add_boolean "success" 1
json_add_string "message" "Module removed"
json_dump
;;
updateModule)
read input
json_load "$input"
local name
json_get_var name name
if [ -z "$name" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Module name is required"
json_dump
exit 0
fi
# Update module in background
/usr/sbin/magicmirrorctl module update "$name" >/tmp/mm-update.log 2>&1 &
json_init
json_add_boolean "success" 1
json_add_string "message" "Module update started"
json_dump
;;
getModuleConfig)
read input
json_load "$input"
local name
json_get_var name name
if [ -z "$name" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Module name is required"
json_dump
exit 0
fi
local modules_path
modules_path=$(get_modules_path)
local readme_content=""
if [ -f "$modules_path/$name/README.md" ]; then
readme_content=$(cat "$modules_path/$name/README.md")
fi
json_init
json_add_boolean "success" 1
json_add_string "readme" "$readme_content"
json_dump
;;
saveConfig)
read input
json_load "$input"
local content
json_get_var content content
if [ -z "$content" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Content is required"
json_dump
exit 0
fi
local config_path
config_path=$(get_config_path)
local config_file="$config_path/config.js"
# Backup current config
if [ -f "$config_file" ]; then
cp "$config_file" "$config_file.backup.$(date +%Y%m%d_%H%M%S)"
fi
# Save new config
echo "$content" > "$config_file"
json_init
json_add_boolean "success" 1
json_add_string "message" "Configuration saved"
json_dump
;;
restartService)
# Restart MagicMirror service
/etc/init.d/magicmirror restart >/dev/null 2>&1 &
json_init
json_add_boolean "success" 1
json_add_string "message" "Service restart initiated"
json_dump
;;
*)
json_init
json_add_boolean "success" 0
json_add_string "error" "Unknown method"
json_dump
;;
esac
;;
esac

View File

@ -1,37 +0,0 @@
{
"admin/secubox/iot/magicmirror": {
"title": "MagicMirror²",
"order": 40,
"action": {
"type": "view",
"path": "magicmirror/overview"
},
"depends": {
"acl": ["luci-app-magicmirror"]
}
},
"admin/secubox/iot/magicmirror/overview": {
"title": "Overview",
"order": 1,
"action": {
"type": "view",
"path": "magicmirror/overview"
}
},
"admin/secubox/iot/magicmirror/modules": {
"title": "Modules",
"order": 2,
"action": {
"type": "view",
"path": "magicmirror/modules"
}
},
"admin/secubox/iot/magicmirror/config": {
"title": "Configuration",
"order": 3,
"action": {
"type": "view",
"path": "magicmirror/config"
}
}
}

View File

@ -1,17 +0,0 @@
{
"luci-app-magicmirror": {
"description": "Grant access to MagicMirror² Manager",
"read": {
"ubus": {
"luci.magicmirror": ["getStatus", "listModules", "getConfig", "getModuleConfig"]
},
"uci": ["magicmirror"]
},
"write": {
"ubus": {
"luci.magicmirror": ["installModule", "removeModule", "updateModule", "saveConfig", "restartService"]
},
"uci": ["magicmirror"]
}
}
}

View File

@ -1,45 +0,0 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-magicmirror
PKG_RELEASE:=2
PKG_VERSION:=1.0.0
PKG_ARCH:=all
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
PKG_LICENSE:=Apache-2.0
include $(INCLUDE_DIR)/package.mk
define Package/secubox-app-magicmirror
SECTION:=utils
CATEGORY:=Utilities
PKGARCH:=all
SUBMENU:=SecuBox Apps
TITLE:=SecuBox MagicMirror² docker app
DEPENDS:=+uci +libuci +dockerd +docker +containerd +jq
endef
define Package/secubox-app-magicmirror/description
Installer, configuration, and service manager for running MagicMirror²
inside Docker on SecuBox-powered OpenWrt systems. Smart mirror platform
with modular display system and web-based module management.
endef
define Package/secubox-app-magicmirror/conffiles
/etc/config/magicmirror
endef
define Build/Compile
endef
define Package/secubox-app-magicmirror/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/etc/config/magicmirror $(1)/etc/config/magicmirror
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/etc/init.d/magicmirror $(1)/etc/init.d/magicmirror
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/usr/sbin/magicmirrorctl $(1)/usr/sbin/magicmirrorctl
endef
$(eval $(call BuildPackage,secubox-app-magicmirror))

View File

@ -1,10 +0,0 @@
config magicmirror 'main'
option enabled '0'
option image 'karsten13/magicmirror:latest'
option config_path '/srv/magicmirror/config'
option modules_path '/srv/magicmirror/modules'
option css_path '/srv/magicmirror/css'
option port '8080'
option timezone 'UTC'
option language 'en'
option units 'metric'

View File

@ -1,23 +0,0 @@
#!/bin/sh /etc/rc.common
START=95
STOP=10
USE_PROCD=1
SERVICE_BIN="/usr/sbin/magicmirrorctl"
start_service() {
procd_open_instance
procd_set_param command "$SERVICE_BIN" service-run
procd_set_param respawn 2000 5 5
procd_close_instance
}
stop_service() {
"$SERVICE_BIN" service-stop >/dev/null 2>&1
}
restart_service() {
stop_service
start_service
}

View File

@ -1,483 +0,0 @@
#!/bin/sh
# SecuBox MagicMirror² manager
CONFIG="magicmirror"
CONTAINER="secbx-magicmirror"
OPKG_UPDATED=0
usage() {
cat <<'USAGE'
Usage: magicmirrorctl <command>
Commands:
install Install prerequisites, prepare directories, pull image
check Run prerequisite checks
update Pull new image and restart
status Show container status
logs Show container logs (use -f to follow)
module Manage MagicMirror modules (see module --help)
config Manage MagicMirror configuration (see config --help)
service-run Internal: run container via procd
service-stop Stop container
USAGE
}
usage_module() {
cat <<'USAGE'
Usage: magicmirrorctl module <command>
Commands:
list List installed modules
install <url> Install module from git URL
remove <name> Remove installed module
update <name> Update module to latest version
config <name> Show module configuration template
Examples:
magicmirrorctl module install https://github.com/MichMich/MMM-WeatherChart
magicmirrorctl module remove MMM-WeatherChart
magicmirrorctl module update MMM-WeatherChart
USAGE
}
usage_config() {
cat <<'USAGE'
Usage: magicmirrorctl config <command>
Commands:
show Show current configuration
edit Edit configuration (opens in vi)
backup Backup current configuration
restore Restore from backup
reset Reset to default configuration
USAGE
}
require_root() { [ "$(id -u)" -eq 0 ]; }
uci_get() { uci -q get ${CONFIG}.main.$1; }
defaults() {
image="$(uci_get image || echo karsten13/magicmirror:latest)"
config_path="$(uci_get config_path || echo /srv/magicmirror/config)"
modules_path="$(uci_get modules_path || echo /srv/magicmirror/modules)"
css_path="$(uci_get css_path || echo /srv/magicmirror/css)"
port="$(uci_get port || echo 8080)"
timezone="$(uci_get timezone || echo UTC)"
language="$(uci_get language || echo en)"
units="$(uci_get units || echo metric)"
}
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
ensure_packages() {
for pkg in "$@"; do
if ! opkg status "$pkg" >/dev/null 2>&1; then
if [ "$OPKG_UPDATED" -eq 0 ]; then
opkg update || return 1
OPKG_UPDATED=1
fi
opkg install "$pkg" || return 1
fi
done
}
check_prereqs() {
defaults
ensure_dir "$config_path"
ensure_dir "$modules_path"
ensure_dir "$css_path"
[ -d /sys/fs/cgroup ] || { echo "[ERROR] /sys/fs/cgroup missing" >&2; return 1; }
ensure_packages dockerd docker containerd jq
/etc/init.d/dockerd enable >/dev/null 2>&1
/etc/init.d/dockerd start >/dev/null 2>&1
}
pull_image() { defaults; docker pull "$image"; }
stop_container() { docker stop "$CONTAINER" >/dev/null 2>&1 || true; docker rm "$CONTAINER" >/dev/null 2>&1 || true; }
create_default_config() {
local config_file="$config_path/config.js"
defaults
cat > "$config_file" <<'CONFIGJS'
/* MagicMirror² Config
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*
* For more information on how you can configure this file
* see https://docs.magicmirror.builders/configuration/introduction.html
* and https://docs.magicmirror.builders/modules/configuration.html
*/
let config = {
address: "0.0.0.0",
port: 8080,
basePath: "/",
ipWhitelist: [],
useHttps: false,
httpsPrivateKey: "",
httpsCertificate: "",
language: "LANG_PLACEHOLDER",
locale: "LANG_PLACEHOLDER",
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
timeFormat: 24,
units: "UNITS_PLACEHOLDER",
modules: [
{
module: "alert",
},
{
module: "updatenotification",
position: "top_bar"
},
{
module: "clock",
position: "top_left"
},
{
module: "calendar",
header: "Upcoming Events",
position: "top_left",
config: {
calendars: [
{
symbol: "calendar-check",
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics"
}
]
}
},
{
module: "compliments",
position: "lower_third"
},
{
module: "weather",
position: "top_right",
config: {
weatherProvider: "openweathermap",
type: "current",
location: "New York",
locationID: "5128581",
apiKey: "YOUR_OPENWEATHER_API_KEY"
}
},
{
module: "weather",
position: "top_right",
header: "Weather Forecast",
config: {
weatherProvider: "openweathermap",
type: "forecast",
location: "New York",
locationID: "5128581",
apiKey: "YOUR_OPENWEATHER_API_KEY"
}
},
{
module: "newsfeed",
position: "bottom_bar",
config: {
feeds: [
{
title: "New York Times",
url: "https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml"
}
],
showSourceTitle: true,
showPublishDate: true,
broadcastNewsFeeds: true,
broadcastNewsUpdates: true
}
},
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = config;}
CONFIGJS
# Replace placeholders
sed -i "s/LANG_PLACEHOLDER/$language/g" "$config_file"
sed -i "s/UNITS_PLACEHOLDER/$units/g" "$config_file"
chmod 644 "$config_file"
echo "Created default config: $config_file"
}
cmd_install() {
require_root || { echo Root required >&2; exit 1; }
check_prereqs || exit 1
pull_image || exit 1
# Create default config if not exists
if [ ! -f "$config_path/config.js" ]; then
create_default_config
fi
uci set ${CONFIG}.main.enabled='1'
uci commit ${CONFIG}
/etc/init.d/magicmirror enable
echo "MagicMirror² prerequisites installed."
echo "Start with: /etc/init.d/magicmirror start"
echo ""
echo "Access MagicMirror at: http://$(uci get network.lan.ipaddr):${port}"
echo "Edit config: $config_path/config.js"
echo "Install modules to: $modules_path"
}
cmd_check() { check_prereqs; echo "Prerequisite check completed."; }
cmd_update() {
require_root || { echo Root required >&2; exit 1; }
pull_image || exit 1
/etc/init.d/magicmirror restart
}
cmd_status() { docker ps -a --filter "name=$CONTAINER"; }
cmd_logs() { docker logs "$@" "$CONTAINER"; }
# Module management commands
cmd_module_list() {
defaults
echo "Installed MagicMirror Modules:"
echo "=============================="
if [ -d "$modules_path" ]; then
for module_dir in "$modules_path"/MMM-*; do
if [ -d "$module_dir" ]; then
local module_name=$(basename "$module_dir")
local module_readme="$module_dir/README.md"
echo " - $module_name"
if [ -f "$module_readme" ]; then
head -5 "$module_readme" | grep -i "description" | sed 's/^/ /'
fi
fi
done
else
echo " No modules directory found"
fi
}
cmd_module_install() {
local git_url="$1"
require_root || { echo Root required >&2; exit 1; }
defaults
if [ -z "$git_url" ]; then
echo "Error: Git URL required" >&2
usage_module
exit 1
fi
# Extract module name from URL
local module_name=$(basename "$git_url" .git)
echo "Installing module: $module_name"
echo "From: $git_url"
# Ensure git is available in container or install locally
if ! command -v git >/dev/null 2>&1; then
ensure_packages git git-http
fi
# Clone module
cd "$modules_path" || exit 1
if [ -d "$module_name" ]; then
echo "Module already exists. Use 'update' to update it."
exit 1
fi
git clone "$git_url" || exit 1
# Run npm install if package.json exists
if [ -f "$module_name/package.json" ]; then
echo "Running npm install..."
docker exec "$CONTAINER" sh -c "cd modules/$module_name && npm install" 2>/dev/null || {
echo "Note: Container not running. Module installed but npm dependencies not installed."
echo "Start MagicMirror and run: magicmirrorctl module update $module_name"
}
fi
echo "Module installed: $module_name"
echo "Add to config.js to activate it"
}
cmd_module_remove() {
local module_name="$1"
require_root || { echo Root required >&2; exit 1; }
defaults
if [ -z "$module_name" ]; then
echo "Error: Module name required" >&2
usage_module
exit 1
fi
local module_dir="$modules_path/$module_name"
if [ ! -d "$module_dir" ]; then
echo "Error: Module not found: $module_name" >&2
exit 1
fi
echo "Removing module: $module_name"
rm -rf "$module_dir"
echo "Module removed. Update config.js to remove module configuration."
}
cmd_module_update() {
local module_name="$1"
require_root || { echo Root required >&2; exit 1; }
defaults
if [ -z "$module_name" ]; then
echo "Error: Module name required" >&2
usage_module
exit 1
fi
local module_dir="$modules_path/$module_name"
if [ ! -d "$module_dir" ]; then
echo "Error: Module not found: $module_name" >&2
exit 1
fi
echo "Updating module: $module_name"
cd "$module_dir" || exit 1
git pull || exit 1
if [ -f "package.json" ]; then
echo "Running npm install..."
docker exec "$CONTAINER" sh -c "cd modules/$module_name && npm install" || {
echo "Warning: Failed to install npm dependencies. Make sure container is running."
}
fi
echo "Module updated: $module_name"
}
cmd_module_config() {
local module_name="$1"
defaults
if [ -z "$module_name" ]; then
echo "Error: Module name required" >&2
usage_module
exit 1
fi
local module_dir="$modules_path/$module_name"
if [ ! -d "$module_dir" ]; then
echo "Error: Module not found: $module_name" >&2
exit 1
fi
# Show README and config template
if [ -f "$module_dir/README.md" ]; then
echo "Module: $module_name"
echo "=============================="
grep -A 20 -i "config" "$module_dir/README.md" | head -30
else
echo "No README.md found for module: $module_name"
fi
}
cmd_module() {
case "${1:-list}" in
list) shift; cmd_module_list "$@" ;;
install) shift; cmd_module_install "$@" ;;
remove) shift; cmd_module_remove "$@" ;;
update) shift; cmd_module_update "$@" ;;
config) shift; cmd_module_config "$@" ;;
help|--help|-h) usage_module ;;
*) echo "Unknown module command: $1" >&2; usage_module >&2; exit 1 ;;
esac
}
# Config management commands
cmd_config_show() {
defaults
cat "$config_path/config.js"
}
cmd_config_edit() {
require_root || { echo Root required >&2; exit 1; }
defaults
${EDITOR:-vi} "$config_path/config.js"
}
cmd_config_backup() {
defaults
local backup_file="$config_path/config.js.backup.$(date +%Y%m%d_%H%M%S)"
cp "$config_path/config.js" "$backup_file"
echo "Backup created: $backup_file"
}
cmd_config_restore() {
require_root || { echo Root required >&2; exit 1; }
defaults
local latest_backup=$(ls -t "$config_path"/config.js.backup.* 2>/dev/null | head -1)
if [ -z "$latest_backup" ]; then
echo "No backups found" >&2
exit 1
fi
cp "$latest_backup" "$config_path/config.js"
echo "Restored from: $latest_backup"
}
cmd_config_reset() {
require_root || { echo Root required >&2; exit 1; }
cmd_config_backup
create_default_config
echo "Configuration reset to defaults"
}
cmd_config() {
case "${1:-show}" in
show) shift; cmd_config_show "$@" ;;
edit) shift; cmd_config_edit "$@" ;;
backup) shift; cmd_config_backup "$@" ;;
restore) shift; cmd_config_restore "$@" ;;
reset) shift; cmd_config_reset "$@" ;;
help|--help|-h) usage_config ;;
*) echo "Unknown config command: $1" >&2; usage_config >&2; exit 1 ;;
esac
}
cmd_service_run() {
require_root || { echo Root required >&2; exit 1; }
check_prereqs || exit 1
defaults
stop_container
local docker_args="--name $CONTAINER"
docker_args="$docker_args -p ${port}:8080"
docker_args="$docker_args -v $config_path:/opt/magic_mirror/config"
docker_args="$docker_args -v $modules_path:/opt/magic_mirror/modules"
docker_args="$docker_args -v $css_path:/opt/magic_mirror/css/custom"
docker_args="$docker_args -e TZ=$timezone"
exec docker run --rm $docker_args "$image"
}
cmd_service_stop() { require_root || { echo Root required >&2; exit 1; }; stop_container; }
case "${1:-}" in
install) shift; cmd_install "$@" ;;
check) shift; cmd_check "$@" ;;
update) shift; cmd_update "$@" ;;
status) shift; cmd_status "$@" ;;
logs) shift; cmd_logs "$@" ;;
module) shift; cmd_module "$@" ;;
config) shift; cmd_config "$@" ;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
help|--help|-h|'') usage ;;
*) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;;
esac

View File

@ -138,10 +138,10 @@ EOF
# Register with rpcd
/etc/init.d/rpcd restart
# Sync component registry from catalog
# Sync component registry from catalog (run in background so installation completes quickly)
if [ -x /usr/sbin/secubox-sync-registry ]; then
echo "Syncing component registry..."
/usr/sbin/secubox-sync-registry sync
echo "Starting component registry sync in background..."
(/usr/sbin/secubox-sync-registry sync &) >/dev/null 2>&1
fi
echo "SecuBox Core Framework v0.9.0 installed successfully"

View File

@ -37,7 +37,8 @@ log_message() {
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$timestamp] [$level] $message" >> "$REGISTRY_LOG"
logger -t secubox-component "[$level] $message"
# Only log INFO and above to syslog (skip DEBUG)
[ "$level" != "DEBUG" ] && logger -t secubox-component "[$level] $message"
}
# Read registry database

View File

@ -22,7 +22,8 @@ log() {
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
logger -t secubox-core -p "user.$level" "$message"
# Only log info and above to syslog (skip debug)
[ "$level" != "debug" ] && logger -t secubox-core -p "user.$level" "$message"
}
# Get system status

View File

@ -39,7 +39,8 @@ log_message() {
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$timestamp] [$level] $message" >> "$STATE_LOG"
logger -t secubox-state "[$level] $message"
# Only log INFO and above to syslog (skip DEBUG)
[ "$level" != "DEBUG" ] && logger -t secubox-state "[$level] $message"
}
# Read state database

View File

@ -13,6 +13,13 @@ PLUGIN_CATALOG_DIR="/usr/share/secubox/plugins/catalog"
REGISTRY_FILE="/var/lib/secubox/component-registry.json"
SYNC_LOG="/var/log/secubox-sync.log"
# Cache opkg output to avoid repeated calls (major performance optimization)
OPKG_CACHE=""
get_opkg_cache() {
[ -z "$OPKG_CACHE" ] && OPKG_CACHE=$(opkg list-installed 2>/dev/null)
echo "$OPKG_CACHE"
}
# Log message
log_message() {
local level="$1"
@ -21,7 +28,8 @@ log_message() {
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$timestamp] [$level] $message" >> "$SYNC_LOG"
logger -t secubox-sync "[$level] $message"
# Only log INFO and above to syslog (skip DEBUG)
[ "$level" != "DEBUG" ] && logger -t secubox-sync "[$level] $message"
}
# Sync catalog apps to component registry
@ -176,8 +184,11 @@ sync_installed_packages() {
log_message "INFO" "Detecting installed packages"
# Get list of SecuBox-related packages
local secubox_packages=$(opkg list-installed | grep -E "^(secubox-|luci-app-|luci-mod-)" | awk '{print $1}')
# Pre-populate the opkg cache (single call instead of multiple)
local opkg_output=$(get_opkg_cache)
# Get list of SecuBox-related packages from cache
local secubox_packages=$(echo "$opkg_output" | grep -E "^(secubox-|luci-app-|luci-mod-)" | awk '{print $1}')
for pkg_name in $secubox_packages; do
# Check if already registered
@ -185,8 +196,8 @@ sync_installed_packages() {
continue
fi
# Get package version
local pkg_version=$(opkg list-installed | grep "^$pkg_name " | awk '{print $3}')
# Get package version from cache
local pkg_version=$(echo "$opkg_output" | grep "^$pkg_name " | awk '{print $3}')
# Register as module component
local metadata=$(cat <<EOF
@ -223,32 +234,10 @@ EOF
}
# Update state references for all registered components
# Note: State initialization is now deferred to first access for performance
# Components will auto-initialize their state when first queried
update_state_references() {
log_message "INFO" "Updating state references"
# Get all registered components
local components=$(/usr/sbin/secubox-component list 2>/dev/null)
if [ -z "$components" ] || [ "$components" = "[]" ]; then
log_message "INFO" "No components to update"
return 0
fi
# For each component, ensure it has a state entry
local component_ids=$(echo "$components" | jq -r '.[].id' 2>/dev/null)
for comp_id in $component_ids; do
# Check if state exists
local state=$(/usr/sbin/secubox-state get "$comp_id" 2>/dev/null)
if [ -z "$state" ] || echo "$state" | grep -q "Error:"; then
# Initialize state as available
/usr/sbin/secubox-state set "$comp_id" available "auto_sync" > /dev/null 2>&1 || true
log_message "DEBUG" "Initialized state for: $comp_id"
fi
done
log_message "INFO" "State references updated"
log_message "INFO" "State references will be initialized on first access (deferred for performance)"
return 0
}

View File

@ -1672,6 +1672,11 @@ CONFIG_PACKAGE_kmod-usb-storage=y
CONFIG_PACKAGE_kmod-fs-ext4=y
CONFIG_PACKAGE_kmod-fs-vfat=y
# Container/LXC support
CONFIG_PACKAGE_kmod-veth=y
CONFIG_PACKAGE_kmod-br-netfilter=y
CONFIG_PACKAGE_kmod-nf-conntrack-netlink=y
# SecuBox packages - Core
CONFIG_PACKAGE_secubox-app=y
CONFIG_PACKAGE_luci-app-secubox=y

View File

@ -0,0 +1,197 @@
#!/bin/bash
#
# expand-openwrt-image.sh
# Downloads OpenWrt ext4 image and expands it to specified size
#
# Usage: ./expand-openwrt-image.sh [SIZE_GB]
# SIZE_GB defaults to 16
set -e
# Configuration
IMAGE_URL="https://downloads.openwrt.org/releases/24.10.5/targets/mvebu/cortexa72/openwrt-24.10.5-mvebu-cortexa72-globalscale_mochabin-ext4-sdcard.img.gz"
SIZE_GB="${1:-16}"
WORK_DIR="${WORK_DIR:-$(pwd)}"
# Derived names
IMAGE_GZ="$(basename "$IMAGE_URL")"
IMAGE_NAME="${IMAGE_GZ%.gz}"
OUTPUT_IMAGE="${IMAGE_NAME%.img}-${SIZE_GB}gb.img"
cd "$WORK_DIR"
echo "=== OpenWrt Image Expansion Script (ext4) ==="
echo "Target size: ${SIZE_GB}GB"
echo "Working directory: $WORK_DIR"
echo ""
# Check required tools
for tool in wget gunzip sfdisk; do
if ! command -v "$tool" &>/dev/null; then
echo "ERROR: Required tool '$tool' not found"
exit 1
fi
done
# Step 1: Download image if not present
if [ -f "$IMAGE_GZ" ]; then
echo "[1/5] Image archive already exists: $IMAGE_GZ"
elif [ -f "$IMAGE_NAME" ]; then
echo "[1/5] Decompressed image already exists: $IMAGE_NAME"
else
echo "[1/5] Downloading image..."
wget -q --show-progress "$IMAGE_URL" -O "$IMAGE_GZ"
fi
# Step 2: Decompress image
if [ -f "$IMAGE_NAME" ]; then
echo "[2/5] Image already decompressed: $IMAGE_NAME"
else
echo "[2/5] Decompressing image..."
gunzip -k "$IMAGE_GZ" 2>/dev/null || gunzip -kf "$IMAGE_GZ" || true
fi
# Step 3: Create expanded copy
echo "[3/5] Creating ${SIZE_GB}GB image: $OUTPUT_IMAGE"
cp "$IMAGE_NAME" "$OUTPUT_IMAGE"
# Calculate target size in bytes
TARGET_BYTES=$((SIZE_GB * 1024 * 1024 * 1024))
# Expand the image file
truncate -s "$TARGET_BYTES" "$OUTPUT_IMAGE"
# Step 4: Expand partition 2 to fill all space
echo "[4/5] Expanding partition 2..."
echo "Original partition layout:"
fdisk -l "$OUTPUT_IMAGE"
echo ""
# Get partition info
PART_INFO=$(sfdisk -d "$OUTPUT_IMAGE" 2>/dev/null)
# Extract disk label-id (critical for PARTUUID)
LABEL_ID=$(echo "$PART_INFO" | grep -E '^label-id:' | sed 's/label-id: *//')
if [ -z "$LABEL_ID" ]; then
echo "WARNING: Could not extract label-id, PARTUUID may change!"
else
echo "Preserving disk label-id: $LABEL_ID"
fi
# Extract partition 1 info
PART1_LINE=$(echo "$PART_INFO" | grep -E '^[^ ]+1 :')
PART1_START=$(echo "$PART1_LINE" | sed -n 's/.*start= *\([0-9]*\).*/\1/p')
PART1_SIZE=$(echo "$PART1_LINE" | sed -n 's/.*size= *\([0-9]*\).*/\1/p')
PART1_TYPE=$(echo "$PART1_LINE" | sed -n 's/.*type= *\([^,]*\).*/\1/p')
PART1_BOOT=$(echo "$PART1_LINE" | grep -q 'bootable' && echo ", bootable" || echo "")
# Extract partition 2 start
PART2_LINE=$(echo "$PART_INFO" | grep -E '^[^ ]+2 :')
PART2_START=$(echo "$PART2_LINE" | sed -n 's/.*start= *\([0-9]*\).*/\1/p')
PART2_TYPE=$(echo "$PART2_LINE" | sed -n 's/.*type= *\([^,]*\).*/\1/p')
if [ -z "$PART2_START" ]; then
echo "ERROR: Could not parse partition 2"
exit 1
fi
echo "Partition 2 starts at sector: $PART2_START"
echo "Expanding partition 2 to fill all remaining space..."
# Create new partition table with expanded partition 2
# Include label-id to preserve PARTUUID
LABEL_ID_LINE=""
if [ -n "$LABEL_ID" ]; then
LABEL_ID_LINE="label-id: $LABEL_ID"
fi
cat <<EOF | sfdisk --no-reread "$OUTPUT_IMAGE"
label: dos
$LABEL_ID_LINE
unit: sectors
${OUTPUT_IMAGE}1 : start=$PART1_START, size=$PART1_SIZE, type=$PART1_TYPE$PART1_BOOT
${OUTPUT_IMAGE}2 : start=$PART2_START, type=$PART2_TYPE
EOF
echo ""
echo "New partition layout:"
fdisk -l "$OUTPUT_IMAGE"
# Verify PARTUUID is preserved
if [ -n "$LABEL_ID" ]; then
NEW_LABEL_ID=$(sfdisk -d "$OUTPUT_IMAGE" 2>/dev/null | grep -E '^label-id:' | sed 's/label-id: *//')
if [ "$LABEL_ID" = "$NEW_LABEL_ID" ]; then
echo ""
echo "PARTUUID preserved: ${LABEL_ID#0x}-01 (boot), ${LABEL_ID#0x}-02 (root)"
else
echo ""
echo "WARNING: label-id changed from $LABEL_ID to $NEW_LABEL_ID"
echo "PARTUUID will be different!"
fi
fi
# Step 5: Resize ext4 filesystem
echo ""
echo "[5/5] Resizing ext4 filesystem..."
PART2_OFFSET=$((PART2_START * 512))
if command -v losetup &>/dev/null && command -v resize2fs &>/dev/null; then
# Calculate partition 2 size
TOTAL_SECTORS=$((TARGET_BYTES / 512))
PART2_SIZE_SECTORS=$((TOTAL_SECTORS - PART2_START))
PART2_SIZE_BYTES=$((PART2_SIZE_SECTORS * 512))
LOOP_DEV=$(losetup -f --show -o "$PART2_OFFSET" --sizelimit "$PART2_SIZE_BYTES" "$OUTPUT_IMAGE" 2>/dev/null || true)
if [ -n "$LOOP_DEV" ]; then
# Check filesystem
FS_TYPE=$(blkid -o value -s TYPE "$LOOP_DEV" 2>/dev/null || echo "unknown")
echo "Filesystem type: $FS_TYPE"
if [ "$FS_TYPE" = "ext4" ]; then
echo "Running e2fsck..."
e2fsck -f -y "$LOOP_DEV" 2>&1 || true
echo "Running resize2fs..."
resize2fs "$LOOP_DEV" 2>&1 || true
echo "Filesystem resized successfully!"
else
echo "Warning: Expected ext4, found $FS_TYPE"
fi
losetup -d "$LOOP_DEV"
else
echo "Note: Could not setup loop device (need root)."
echo " Resize filesystem after flashing:"
echo " resize2fs /dev/mmcblk0p2"
fi
else
echo "Note: losetup or resize2fs not available."
echo " Resize filesystem after flashing:"
echo " resize2fs /dev/mmcblk0p2"
fi
echo ""
echo "=== Compressing image ==="
echo "Compressing to ${OUTPUT_IMAGE}.gz (this may take a while)..."
gzip -f "$OUTPUT_IMAGE"
echo "Generating checksums..."
sha256sum "${OUTPUT_IMAGE}.gz" > "${OUTPUT_IMAGE}.gz.sha256"
md5sum "${OUTPUT_IMAGE}.gz" > "${OUTPUT_IMAGE}.gz.md5"
echo ""
echo "=== Complete ==="
echo "Expanded image created: ${OUTPUT_IMAGE}.gz"
echo "Compressed size: $(du -h "${OUTPUT_IMAGE}.gz" | cut -f1)"
echo ""
echo "Checksums:"
cat "${OUTPUT_IMAGE}.gz.sha256"
cat "${OUTPUT_IMAGE}.gz.md5"
echo ""
echo "To flash to SD card (replace /dev/sdX with your device):"
echo " gunzip -c ${OUTPUT_IMAGE}.gz | sudo dd of=/dev/sdX bs=4M status=progress conv=fsync"
echo ""
echo "Or use balenaEtcher/Raspberry Pi Imager (supports .gz directly)."