go
This commit is contained in:
parent
d80501b33a
commit
3132ef9c14
@ -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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
@ -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'
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
197
secubox-tools/scripts/expand-openwrt-image.sh
Executable file
197
secubox-tools/scripts/expand-openwrt-image.sh
Executable 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)."
|
||||
Loading…
Reference in New Issue
Block a user