go
This commit is contained in:
parent
d80501b33a
commit
3132ef9c14
@ -107,7 +107,17 @@
|
|||||||
"WebFetch(domain:hub.docker.com)",
|
"WebFetch(domain:hub.docker.com)",
|
||||||
"WebFetch(domain:localai.io)",
|
"WebFetch(domain:localai.io)",
|
||||||
"WebFetch(domain:downloads.lms-community.org)",
|
"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
|
# Register with rpcd
|
||||||
/etc/init.d/rpcd restart
|
/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
|
if [ -x /usr/sbin/secubox-sync-registry ]; then
|
||||||
echo "Syncing component registry..."
|
echo "Starting component registry sync in background..."
|
||||||
/usr/sbin/secubox-sync-registry sync
|
(/usr/sbin/secubox-sync-registry sync &) >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "SecuBox Core Framework v0.9.0 installed successfully"
|
echo "SecuBox Core Framework v0.9.0 installed successfully"
|
||||||
|
|||||||
@ -37,7 +37,8 @@ log_message() {
|
|||||||
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
|
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
echo "[$timestamp] [$level] $message" >> "$REGISTRY_LOG"
|
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
|
# Read registry database
|
||||||
|
|||||||
@ -22,7 +22,8 @@ log() {
|
|||||||
local message="$*"
|
local message="$*"
|
||||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
|
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
|
# Get system status
|
||||||
|
|||||||
@ -39,7 +39,8 @@ log_message() {
|
|||||||
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
|
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
echo "[$timestamp] [$level] $message" >> "$STATE_LOG"
|
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
|
# Read state database
|
||||||
|
|||||||
@ -13,6 +13,13 @@ PLUGIN_CATALOG_DIR="/usr/share/secubox/plugins/catalog"
|
|||||||
REGISTRY_FILE="/var/lib/secubox/component-registry.json"
|
REGISTRY_FILE="/var/lib/secubox/component-registry.json"
|
||||||
SYNC_LOG="/var/log/secubox-sync.log"
|
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
|
||||||
log_message() {
|
log_message() {
|
||||||
local level="$1"
|
local level="$1"
|
||||||
@ -21,7 +28,8 @@ log_message() {
|
|||||||
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
|
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
echo "[$timestamp] [$level] $message" >> "$SYNC_LOG"
|
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
|
# Sync catalog apps to component registry
|
||||||
@ -176,8 +184,11 @@ sync_installed_packages() {
|
|||||||
|
|
||||||
log_message "INFO" "Detecting installed packages"
|
log_message "INFO" "Detecting installed packages"
|
||||||
|
|
||||||
# Get list of SecuBox-related packages
|
# Pre-populate the opkg cache (single call instead of multiple)
|
||||||
local secubox_packages=$(opkg list-installed | grep -E "^(secubox-|luci-app-|luci-mod-)" | awk '{print $1}')
|
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
|
for pkg_name in $secubox_packages; do
|
||||||
# Check if already registered
|
# Check if already registered
|
||||||
@ -185,8 +196,8 @@ sync_installed_packages() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get package version
|
# Get package version from cache
|
||||||
local pkg_version=$(opkg list-installed | grep "^$pkg_name " | awk '{print $3}')
|
local pkg_version=$(echo "$opkg_output" | grep "^$pkg_name " | awk '{print $3}')
|
||||||
|
|
||||||
# Register as module component
|
# Register as module component
|
||||||
local metadata=$(cat <<EOF
|
local metadata=$(cat <<EOF
|
||||||
@ -223,32 +234,10 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Update state references for all registered components
|
# 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() {
|
update_state_references() {
|
||||||
log_message "INFO" "Updating state references"
|
log_message "INFO" "State references will be initialized on first access (deferred for performance)"
|
||||||
|
|
||||||
# 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"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1672,6 +1672,11 @@ CONFIG_PACKAGE_kmod-usb-storage=y
|
|||||||
CONFIG_PACKAGE_kmod-fs-ext4=y
|
CONFIG_PACKAGE_kmod-fs-ext4=y
|
||||||
CONFIG_PACKAGE_kmod-fs-vfat=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
|
# SecuBox packages - Core
|
||||||
CONFIG_PACKAGE_secubox-app=y
|
CONFIG_PACKAGE_secubox-app=y
|
||||||
CONFIG_PACKAGE_luci-app-secubox=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