feat(picobrew): Add PicoBrew Server packages for OpenWrt
Add two new packages for self-hosted brewing controller support: secubox-app-picobrew: - LXC container-based PicoBrew Server installation - Alpine Linux rootfs with Python/Flask environment - UCI configuration for port, memory, brewing defaults - procd service management with respawn - Commands: install, uninstall, update, status, logs, shell luci-app-picobrew: - Modern dashboard UI with SecuBox styling - Service controls (start/stop/restart/install/update) - Real-time status monitoring and logs - Settings page for server and brewing configuration - RPCD backend with full API coverage Supports PicoBrew Zymatic, Z, Pico C, and Pico Pro devices. Repository: https://github.com/CyberMind-FR/picobrew-server Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
14690ebe9e
commit
b69a84394b
29
package/secubox/luci-app-picobrew/Makefile
Normal file
29
package/secubox/luci-app-picobrew/Makefile
Normal file
@ -0,0 +1,29 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
#
|
||||
# LuCI PicoBrew Dashboard
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-picobrew
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_ARCH:=all
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
|
||||
LUCI_TITLE:=LuCI PicoBrew Dashboard
|
||||
LUCI_DESCRIPTION:=Modern dashboard for PicoBrew Server management on OpenWrt
|
||||
LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +rpcd +rpcd-mod-luci +secubox-app-picobrew
|
||||
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_FILE_MODES:=/usr/libexec/rpcd/luci.picobrew:root:root:755
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# Note: /etc/config/picobrew is in secubox-app-picobrew
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-picobrew))
|
||||
@ -0,0 +1,803 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
|
||||
// RPC declarations
|
||||
var callGetStatus = rpc.declare({
|
||||
object: 'luci.picobrew',
|
||||
method: 'get_status',
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callGetConfig = rpc.declare({
|
||||
object: 'luci.picobrew',
|
||||
method: 'get_config',
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callStart = rpc.declare({
|
||||
object: 'luci.picobrew',
|
||||
method: 'start',
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callStop = rpc.declare({
|
||||
object: 'luci.picobrew',
|
||||
method: 'stop',
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callRestart = rpc.declare({
|
||||
object: 'luci.picobrew',
|
||||
method: 'restart',
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callInstall = rpc.declare({
|
||||
object: 'luci.picobrew',
|
||||
method: 'install',
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callUninstall = rpc.declare({
|
||||
object: 'luci.picobrew',
|
||||
method: 'uninstall',
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callUpdate = rpc.declare({
|
||||
object: 'luci.picobrew',
|
||||
method: 'update',
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callGetLogs = rpc.declare({
|
||||
object: 'luci.picobrew',
|
||||
method: 'get_logs',
|
||||
params: ['lines'],
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callGetInstallProgress = rpc.declare({
|
||||
object: 'luci.picobrew',
|
||||
method: 'get_install_progress',
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callGetSessions = rpc.declare({
|
||||
object: 'luci.picobrew',
|
||||
method: 'get_sessions',
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callGetRecipes = rpc.declare({
|
||||
object: 'luci.picobrew',
|
||||
method: 'get_recipes',
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
// CSS styles
|
||||
var styles = `
|
||||
.picobrew-dashboard {
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.pb-header {
|
||||
background: linear-gradient(135deg, rgba(6, 182, 212, 0.1), rgba(139, 92, 246, 0.1));
|
||||
border: 1px solid rgba(6, 182, 212, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.pb-header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.pb-logo {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.pb-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #06b6d4;
|
||||
}
|
||||
|
||||
.pb-subtitle {
|
||||
margin: 4px 0 0 0;
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pb-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.pb-stat-card {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid rgba(51, 65, 85, 0.5);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pb-stat-card.success { border-color: rgba(16, 185, 129, 0.5); }
|
||||
.pb-stat-card.warning { border-color: rgba(245, 158, 11, 0.5); }
|
||||
.pb-stat-card.error { border-color: rgba(244, 63, 94, 0.5); }
|
||||
|
||||
.pb-stat-icon {
|
||||
font-size: 32px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(6, 182, 212, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.pb-stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pb-stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.pb-stat-label {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.pb-main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.pb-main-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.pb-card {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid rgba(51, 65, 85, 0.5);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pb-card-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid rgba(51, 65, 85, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pb-card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pb-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.pb-btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pb-btn-primary {
|
||||
background: linear-gradient(135deg, #06b6d4, #0891b2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pb-btn-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.4);
|
||||
}
|
||||
|
||||
.pb-btn-success {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pb-btn-danger {
|
||||
background: linear-gradient(135deg, #f43f5e, #e11d48);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pb-btn-warning {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pb-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.pb-btn-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pb-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pb-status-badge.running {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.pb-status-badge.stopped {
|
||||
background: rgba(244, 63, 94, 0.2);
|
||||
color: #f43f5e;
|
||||
}
|
||||
|
||||
.pb-status-badge.not-installed {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.pb-info-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pb-info-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid rgba(51, 65, 85, 0.3);
|
||||
}
|
||||
|
||||
.pb-info-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.pb-info-label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.pb-info-value {
|
||||
color: #f1f5f9;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pb-info-value a {
|
||||
color: #06b6d4;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pb-info-value a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.pb-logs {
|
||||
background: #0f172a;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-family: "Monaco", "Consolas", monospace;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pb-logs-line {
|
||||
margin: 4px 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.pb-progress {
|
||||
background: rgba(51, 65, 85, 0.5);
|
||||
border-radius: 8px;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.pb-progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #06b6d4, #8b5cf6);
|
||||
border-radius: 8px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.pb-progress-text {
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.pb-empty {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pb-empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
return view.extend({
|
||||
statusData: null,
|
||||
configData: null,
|
||||
logsData: null,
|
||||
installProgress: null,
|
||||
sessionsData: null,
|
||||
recipesData: null,
|
||||
|
||||
load: function() {
|
||||
return this.refreshData();
|
||||
},
|
||||
|
||||
refreshData: function() {
|
||||
var self = this;
|
||||
return Promise.all([
|
||||
callGetStatus(),
|
||||
callGetConfig(),
|
||||
callGetLogs(50),
|
||||
callGetSessions(),
|
||||
callGetRecipes()
|
||||
]).then(function(data) {
|
||||
self.statusData = data[0] || {};
|
||||
self.configData = data[1] || {};
|
||||
self.logsData = data[2] || {};
|
||||
self.sessionsData = data[3] || {};
|
||||
self.recipesData = data[4] || {};
|
||||
return data;
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
|
||||
// Inject styles
|
||||
var styleEl = E('style', {}, styles);
|
||||
|
||||
var container = E('div', { 'class': 'picobrew-dashboard' }, [
|
||||
styleEl,
|
||||
this.renderHeader(),
|
||||
this.renderStatsGrid(),
|
||||
this.renderMainGrid()
|
||||
]);
|
||||
|
||||
// Poll for updates
|
||||
poll.add(function() {
|
||||
return self.refreshData().then(function() {
|
||||
self.updateDynamicContent();
|
||||
});
|
||||
}, 10);
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
renderHeader: function() {
|
||||
var status = this.statusData;
|
||||
var statusClass = !status.installed ? 'not-installed' : (status.running ? 'running' : 'stopped');
|
||||
var statusText = !status.installed ? _('Not Installed') : (status.running ? _('Running') : _('Stopped'));
|
||||
|
||||
return E('div', { 'class': 'pb-header' }, [
|
||||
E('div', { 'class': 'pb-header-content' }, [
|
||||
E('div', { 'class': 'pb-logo' }, '🍺'),
|
||||
E('div', {}, [
|
||||
E('h1', { 'class': 'pb-title' }, _('PicoBrew Server')),
|
||||
E('p', { 'class': 'pb-subtitle' }, _('Self-hosted brewing controller for PicoBrew devices'))
|
||||
]),
|
||||
E('div', { 'class': 'pb-status-badge ' + statusClass, 'id': 'pb-status-badge' }, [
|
||||
E('span', {}, statusClass === 'running' ? '●' : '○'),
|
||||
statusText
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderStatsGrid: function() {
|
||||
var status = this.statusData;
|
||||
var sessions = (this.sessionsData.sessions || []).length;
|
||||
var recipes = (this.recipesData.recipes || []).length;
|
||||
|
||||
var stats = [
|
||||
{
|
||||
icon: '🔌',
|
||||
label: _('Status'),
|
||||
value: status.running ? _('Online') : _('Offline'),
|
||||
id: 'stat-status',
|
||||
cardClass: status.running ? 'success' : 'error'
|
||||
},
|
||||
{
|
||||
icon: '🌐',
|
||||
label: _('Port'),
|
||||
value: status.http_port || '8080',
|
||||
id: 'stat-port'
|
||||
},
|
||||
{
|
||||
icon: '📊',
|
||||
label: _('Sessions'),
|
||||
value: sessions,
|
||||
id: 'stat-sessions'
|
||||
},
|
||||
{
|
||||
icon: '📖',
|
||||
label: _('Recipes'),
|
||||
value: recipes,
|
||||
id: 'stat-recipes'
|
||||
}
|
||||
];
|
||||
|
||||
return E('div', { 'class': 'pb-stats-grid' },
|
||||
stats.map(function(stat) {
|
||||
return E('div', { 'class': 'pb-stat-card ' + (stat.cardClass || '') }, [
|
||||
E('div', { 'class': 'pb-stat-icon' }, stat.icon),
|
||||
E('div', { 'class': 'pb-stat-content' }, [
|
||||
E('div', { 'class': 'pb-stat-value', 'id': stat.id }, String(stat.value)),
|
||||
E('div', { 'class': 'pb-stat-label' }, stat.label)
|
||||
])
|
||||
]);
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
renderMainGrid: function() {
|
||||
return E('div', { 'class': 'pb-main-grid' }, [
|
||||
this.renderControlCard(),
|
||||
this.renderInfoCard(),
|
||||
this.renderLogsCard()
|
||||
]);
|
||||
},
|
||||
|
||||
renderControlCard: function() {
|
||||
var self = this;
|
||||
var status = this.statusData;
|
||||
|
||||
var buttons = [];
|
||||
|
||||
if (!status.installed) {
|
||||
buttons.push(
|
||||
E('button', {
|
||||
'class': 'pb-btn pb-btn-primary',
|
||||
'id': 'btn-install',
|
||||
'click': function() { self.handleInstall(); }
|
||||
}, [E('span', {}, '📥'), _('Install')])
|
||||
);
|
||||
} else {
|
||||
if (status.running) {
|
||||
buttons.push(
|
||||
E('button', {
|
||||
'class': 'pb-btn pb-btn-danger',
|
||||
'id': 'btn-stop',
|
||||
'click': function() { self.handleStop(); }
|
||||
}, [E('span', {}, '⏹'), _('Stop')])
|
||||
);
|
||||
buttons.push(
|
||||
E('button', {
|
||||
'class': 'pb-btn pb-btn-warning',
|
||||
'id': 'btn-restart',
|
||||
'click': function() { self.handleRestart(); }
|
||||
}, [E('span', {}, '🔄'), _('Restart')])
|
||||
);
|
||||
} else {
|
||||
buttons.push(
|
||||
E('button', {
|
||||
'class': 'pb-btn pb-btn-success',
|
||||
'id': 'btn-start',
|
||||
'click': function() { self.handleStart(); }
|
||||
}, [E('span', {}, '▶'), _('Start')])
|
||||
);
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
E('button', {
|
||||
'class': 'pb-btn pb-btn-primary',
|
||||
'id': 'btn-update',
|
||||
'click': function() { self.handleUpdate(); }
|
||||
}, [E('span', {}, '⬆'), _('Update')])
|
||||
);
|
||||
|
||||
buttons.push(
|
||||
E('button', {
|
||||
'class': 'pb-btn pb-btn-danger',
|
||||
'id': 'btn-uninstall',
|
||||
'click': function() { self.handleUninstall(); }
|
||||
}, [E('span', {}, '🗑'), _('Uninstall')])
|
||||
);
|
||||
}
|
||||
|
||||
return E('div', { 'class': 'pb-card' }, [
|
||||
E('div', { 'class': 'pb-card-header' }, [
|
||||
E('div', { 'class': 'pb-card-title' }, [
|
||||
E('span', {}, '🎮'),
|
||||
_('Controls')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'pb-card-body' }, [
|
||||
E('div', { 'class': 'pb-btn-group', 'id': 'pb-controls' }, buttons),
|
||||
E('div', { 'class': 'pb-progress', 'id': 'pb-progress-container', 'style': 'display:none' }, [
|
||||
E('div', { 'class': 'pb-progress-bar', 'id': 'pb-progress-bar', 'style': 'width:0%' })
|
||||
]),
|
||||
E('div', { 'class': 'pb-progress-text', 'id': 'pb-progress-text', 'style': 'display:none' })
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderInfoCard: function() {
|
||||
var status = this.statusData;
|
||||
|
||||
var infoItems = [
|
||||
{ label: _('Container'), value: status.container_name || 'picobrew' },
|
||||
{ label: _('Data Path'), value: status.data_path || '/srv/picobrew' },
|
||||
{ label: _('Memory Limit'), value: status.memory_limit || '512M' },
|
||||
{ label: _('Web Interface'), value: status.web_url, isLink: true }
|
||||
];
|
||||
|
||||
return E('div', { 'class': 'pb-card' }, [
|
||||
E('div', { 'class': 'pb-card-header' }, [
|
||||
E('div', { 'class': 'pb-card-title' }, [
|
||||
E('span', {}, 'ℹ️'),
|
||||
_('Information')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'pb-card-body' }, [
|
||||
E('ul', { 'class': 'pb-info-list', 'id': 'pb-info-list' },
|
||||
infoItems.map(function(item) {
|
||||
var valueEl;
|
||||
if (item.isLink && item.value) {
|
||||
valueEl = E('a', { 'href': item.value, 'target': '_blank' }, item.value);
|
||||
} else {
|
||||
valueEl = item.value || '-';
|
||||
}
|
||||
return E('li', {}, [
|
||||
E('span', { 'class': 'pb-info-label' }, item.label),
|
||||
E('span', { 'class': 'pb-info-value' }, valueEl)
|
||||
]);
|
||||
})
|
||||
)
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderLogsCard: function() {
|
||||
var logs = this.logsData.logs || [];
|
||||
|
||||
return E('div', { 'class': 'pb-card', 'style': 'grid-column: span 2' }, [
|
||||
E('div', { 'class': 'pb-card-header' }, [
|
||||
E('div', { 'class': 'pb-card-title' }, [
|
||||
E('span', {}, '📜'),
|
||||
_('Logs')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'pb-card-body' }, [
|
||||
logs.length > 0 ?
|
||||
E('div', { 'class': 'pb-logs', 'id': 'pb-logs' },
|
||||
logs.map(function(line) {
|
||||
return E('div', { 'class': 'pb-logs-line' }, line);
|
||||
})
|
||||
) :
|
||||
E('div', { 'class': 'pb-empty' }, [
|
||||
E('div', { 'class': 'pb-empty-icon' }, '📭'),
|
||||
E('div', {}, _('No logs available'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
updateDynamicContent: function() {
|
||||
var status = this.statusData;
|
||||
|
||||
// Update status badge
|
||||
var badge = document.getElementById('pb-status-badge');
|
||||
if (badge) {
|
||||
var statusClass = !status.installed ? 'not-installed' : (status.running ? 'running' : 'stopped');
|
||||
var statusText = !status.installed ? _('Not Installed') : (status.running ? _('Running') : _('Stopped'));
|
||||
badge.className = 'pb-status-badge ' + statusClass;
|
||||
badge.innerHTML = '';
|
||||
badge.appendChild(E('span', {}, statusClass === 'running' ? '●' : '○'));
|
||||
badge.appendChild(document.createTextNode(' ' + statusText));
|
||||
}
|
||||
|
||||
// Update stats
|
||||
var statStatus = document.getElementById('stat-status');
|
||||
if (statStatus) {
|
||||
statStatus.textContent = status.running ? _('Online') : _('Offline');
|
||||
}
|
||||
|
||||
// Update logs
|
||||
var logsContainer = document.getElementById('pb-logs');
|
||||
if (logsContainer && this.logsData.logs) {
|
||||
logsContainer.innerHTML = '';
|
||||
this.logsData.logs.forEach(function(line) {
|
||||
logsContainer.appendChild(E('div', { 'class': 'pb-logs-line' }, line));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleInstall: function() {
|
||||
var self = this;
|
||||
var btn = document.getElementById('btn-install');
|
||||
if (btn) btn.disabled = true;
|
||||
|
||||
ui.showModal(_('Installing PicoBrew Server'), [
|
||||
E('p', {}, _('This will download and install PicoBrew Server in an LXC container. This may take several minutes.')),
|
||||
E('div', { 'class': 'pb-progress' }, [
|
||||
E('div', { 'class': 'pb-progress-bar', 'id': 'modal-progress', 'style': 'width:0%' })
|
||||
]),
|
||||
E('p', { 'id': 'modal-status' }, _('Starting installation...'))
|
||||
]);
|
||||
|
||||
callInstall().then(function(result) {
|
||||
if (result && result.started) {
|
||||
self.pollInstallProgress();
|
||||
} else {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, result.message || _('Installation failed')), 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, _('Installation failed: ') + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
pollInstallProgress: function() {
|
||||
var self = this;
|
||||
|
||||
var checkProgress = function() {
|
||||
callGetInstallProgress().then(function(result) {
|
||||
var progressBar = document.getElementById('modal-progress');
|
||||
var statusText = document.getElementById('modal-status');
|
||||
|
||||
if (progressBar) {
|
||||
progressBar.style.width = (result.progress || 0) + '%';
|
||||
}
|
||||
if (statusText) {
|
||||
statusText.textContent = result.message || '';
|
||||
}
|
||||
|
||||
if (result.status === 'completed') {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, _('PicoBrew Server installed successfully!')), 'success');
|
||||
self.refreshData();
|
||||
} else if (result.status === 'error') {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, _('Installation failed: ') + result.message), 'error');
|
||||
} else if (result.status === 'running') {
|
||||
setTimeout(checkProgress, 3000);
|
||||
} else {
|
||||
setTimeout(checkProgress, 3000);
|
||||
}
|
||||
}).catch(function() {
|
||||
setTimeout(checkProgress, 5000);
|
||||
});
|
||||
};
|
||||
|
||||
setTimeout(checkProgress, 2000);
|
||||
},
|
||||
|
||||
handleStart: function() {
|
||||
var self = this;
|
||||
callStart().then(function(result) {
|
||||
if (result && result.success) {
|
||||
ui.addNotification(null, E('p', {}, _('PicoBrew Server started')), 'success');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, result.message || _('Failed to start')), 'error');
|
||||
}
|
||||
self.refreshData();
|
||||
});
|
||||
},
|
||||
|
||||
handleStop: function() {
|
||||
var self = this;
|
||||
callStop().then(function(result) {
|
||||
if (result && result.success) {
|
||||
ui.addNotification(null, E('p', {}, _('PicoBrew Server stopped')), 'info');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, result.message || _('Failed to stop')), 'error');
|
||||
}
|
||||
self.refreshData();
|
||||
});
|
||||
},
|
||||
|
||||
handleRestart: function() {
|
||||
var self = this;
|
||||
callRestart().then(function(result) {
|
||||
if (result && result.success) {
|
||||
ui.addNotification(null, E('p', {}, _('PicoBrew Server restarted')), 'success');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, result.message || _('Failed to restart')), 'error');
|
||||
}
|
||||
self.refreshData();
|
||||
});
|
||||
},
|
||||
|
||||
handleUpdate: function() {
|
||||
var self = this;
|
||||
|
||||
ui.showModal(_('Updating PicoBrew Server'), [
|
||||
E('p', {}, _('Updating PicoBrew Server to the latest version...')),
|
||||
E('div', { 'class': 'spinner' })
|
||||
]);
|
||||
|
||||
callUpdate().then(function(result) {
|
||||
ui.hideModal();
|
||||
if (result && result.started) {
|
||||
ui.addNotification(null, E('p', {}, _('Update started. The server will restart automatically.')), 'info');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, result.message || _('Update failed')), 'error');
|
||||
}
|
||||
self.refreshData();
|
||||
});
|
||||
},
|
||||
|
||||
handleUninstall: function() {
|
||||
var self = this;
|
||||
|
||||
ui.showModal(_('Confirm Uninstall'), [
|
||||
E('p', {}, _('Are you sure you want to uninstall PicoBrew Server? Your data will be preserved.')),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-negative',
|
||||
'click': function() {
|
||||
ui.hideModal();
|
||||
callUninstall().then(function(result) {
|
||||
if (result && result.success) {
|
||||
ui.addNotification(null, E('p', {}, _('PicoBrew Server uninstalled')), 'info');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, result.message || _('Uninstall failed')), 'error');
|
||||
}
|
||||
self.refreshData();
|
||||
});
|
||||
}
|
||||
}, _('Uninstall'))
|
||||
])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,133 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
|
||||
var callRestart = rpc.declare({
|
||||
object: 'luci.picobrew',
|
||||
method: 'restart',
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('picobrew', _('PicoBrew Settings'),
|
||||
_('Configure PicoBrew Server settings. Changes require a service restart to take effect.'));
|
||||
|
||||
// Main settings section
|
||||
s = m.section(form.TypedSection, 'picobrew', _('Server Settings'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable Service'),
|
||||
_('Enable or disable the PicoBrew Server service'));
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'http_port', _('HTTP Port'),
|
||||
_('Port for the PicoBrew web interface'));
|
||||
o.datatype = 'port';
|
||||
o.default = '8080';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'http_host', _('Listen Address'),
|
||||
_('IP address to listen on (0.0.0.0 for all interfaces)'));
|
||||
o.datatype = 'ipaddr';
|
||||
o.default = '0.0.0.0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'data_path', _('Data Path'),
|
||||
_('Path for storing recipes, sessions, and logs'));
|
||||
o.default = '/srv/picobrew';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.ListValue, 'memory_limit', _('Memory Limit'),
|
||||
_('Maximum memory for the container'));
|
||||
o.value('256M', '256 MB');
|
||||
o.value('512M', '512 MB');
|
||||
o.value('768M', '768 MB');
|
||||
o.value('1G', '1 GB');
|
||||
o.default = '512M';
|
||||
|
||||
o = s.option(form.ListValue, 'log_level', _('Log Level'),
|
||||
_('Logging verbosity'));
|
||||
o.value('DEBUG', 'Debug');
|
||||
o.value('INFO', 'Info');
|
||||
o.value('WARNING', 'Warning');
|
||||
o.value('ERROR', 'Error');
|
||||
o.default = 'INFO';
|
||||
|
||||
// Server section (HTTPS)
|
||||
s = m.section(form.TypedSection, 'server', _('HTTPS Settings'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Value, 'dns_name', _('DNS Name'),
|
||||
_('Domain name for accessing the server (optional)'));
|
||||
o.placeholder = 'picobrew.local';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Flag, 'https_enabled', _('Enable HTTPS'),
|
||||
_('Enable HTTPS (requires certificates)'));
|
||||
o.default = '0';
|
||||
|
||||
o = s.option(form.Value, 'cert_path', _('Certificate Path'),
|
||||
_('Path to SSL certificate file'));
|
||||
o.depends('https_enabled', '1');
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'key_path', _('Key Path'),
|
||||
_('Path to SSL private key file'));
|
||||
o.depends('https_enabled', '1');
|
||||
o.rmempty = true;
|
||||
|
||||
// Brewing defaults section
|
||||
s = m.section(form.TypedSection, 'brewing', _('Brewing Defaults'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.ListValue, 'units', _('Units'),
|
||||
_('Temperature and measurement units'));
|
||||
o.value('metric', 'Metric (°C, L, kg)');
|
||||
o.value('imperial', 'Imperial (°F, gal, lb)');
|
||||
o.default = 'metric';
|
||||
|
||||
o = s.option(form.Value, 'default_boil_temp', _('Default Boil Temperature'),
|
||||
_('Default boiling temperature'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '100';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'default_mash_temp', _('Default Mash Temperature'),
|
||||
_('Default mashing temperature'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '67';
|
||||
o.rmempty = false;
|
||||
|
||||
return m.render().then(function(mapEl) {
|
||||
// Add restart button after the form
|
||||
var restartBtn = E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'style': 'margin-top: 1em;',
|
||||
'click': function() {
|
||||
ui.showModal(_('Restarting Service'), [
|
||||
E('p', { 'class': 'spinning' }, _('Restarting PicoBrew Server...'))
|
||||
]);
|
||||
callRestart().then(function() {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, _('Service restarted successfully')), 'success');
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, _('Failed to restart: ') + err.message), 'error');
|
||||
});
|
||||
}
|
||||
}, _('Restart Service'));
|
||||
|
||||
var wrapper = E('div', {}, [mapEl, restartBtn]);
|
||||
return wrapper;
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,462 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# LuCI RPC backend for PicoBrew Server
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
|
||||
. /lib/functions.sh
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
CONFIG="picobrew"
|
||||
LXC_NAME="picobrew"
|
||||
LXC_PATH="/srv/lxc"
|
||||
REPO_PATH="/srv/picobrew/app"
|
||||
|
||||
# JSON helpers
|
||||
json_init_obj() { json_init; json_add_object "result"; }
|
||||
json_close_obj() { json_close_object; json_dump; }
|
||||
|
||||
json_error() {
|
||||
json_init
|
||||
json_add_object "error"
|
||||
json_add_string "message" "$1"
|
||||
json_close_object
|
||||
json_dump
|
||||
}
|
||||
|
||||
json_success() {
|
||||
json_init_obj
|
||||
json_add_boolean "success" 1
|
||||
[ -n "$1" ] && json_add_string "message" "$1"
|
||||
json_close_obj
|
||||
}
|
||||
|
||||
# Check if container is running
|
||||
lxc_running() {
|
||||
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
|
||||
}
|
||||
|
||||
# Check if container exists
|
||||
lxc_exists() {
|
||||
[ -f "$LXC_PATH/$LXC_NAME/config" ] && [ -d "$LXC_PATH/$LXC_NAME/rootfs" ]
|
||||
}
|
||||
|
||||
# Get service status
|
||||
get_status() {
|
||||
local enabled running installed uptime
|
||||
local http_port data_path memory_limit
|
||||
|
||||
config_load "$CONFIG"
|
||||
config_get enabled main enabled "0"
|
||||
config_get http_port main http_port "8080"
|
||||
config_get data_path main data_path "/srv/picobrew"
|
||||
config_get memory_limit main memory_limit "512M"
|
||||
|
||||
running="false"
|
||||
installed="false"
|
||||
uptime=""
|
||||
|
||||
if lxc_exists; then
|
||||
installed="true"
|
||||
fi
|
||||
|
||||
if lxc_running; then
|
||||
running="true"
|
||||
uptime=$(lxc-info -n "$LXC_NAME" 2>/dev/null | grep -i "cpu use" | head -1 | awk '{print $3}')
|
||||
fi
|
||||
|
||||
# Check if repo exists
|
||||
local repo_installed="false"
|
||||
[ -d "$REPO_PATH/.git" ] && repo_installed="true"
|
||||
|
||||
# Get LAN IP for URL
|
||||
local lan_ip
|
||||
lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||
|
||||
json_init_obj
|
||||
json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )"
|
||||
json_add_boolean "running" "$( [ "$running" = "true" ] && echo 1 || echo 0 )"
|
||||
json_add_boolean "installed" "$( [ "$installed" = "true" ] && echo 1 || echo 0 )"
|
||||
json_add_boolean "repo_installed" "$( [ "$repo_installed" = "true" ] && echo 1 || echo 0 )"
|
||||
json_add_string "uptime" "$uptime"
|
||||
json_add_int "http_port" "$http_port"
|
||||
json_add_string "data_path" "$data_path"
|
||||
json_add_string "memory_limit" "$memory_limit"
|
||||
json_add_string "web_url" "http://${lan_ip}:${http_port}"
|
||||
json_add_string "container_name" "$LXC_NAME"
|
||||
json_close_obj
|
||||
}
|
||||
|
||||
# Get configuration
|
||||
get_config() {
|
||||
local http_port http_host data_path memory_limit log_level
|
||||
local dns_name https_enabled cert_path key_path
|
||||
local default_boil_temp default_mash_temp units
|
||||
|
||||
config_load "$CONFIG"
|
||||
|
||||
# Main settings
|
||||
config_get http_port main http_port "8080"
|
||||
config_get http_host main http_host "0.0.0.0"
|
||||
config_get data_path main data_path "/srv/picobrew"
|
||||
config_get memory_limit main memory_limit "512M"
|
||||
config_get log_level main log_level "INFO"
|
||||
config_get enabled main enabled "0"
|
||||
|
||||
# Server settings
|
||||
config_get dns_name server dns_name ""
|
||||
config_get https_enabled server https_enabled "0"
|
||||
config_get cert_path server cert_path ""
|
||||
config_get key_path server key_path ""
|
||||
|
||||
# Brewing settings
|
||||
config_get default_boil_temp brewing default_boil_temp "100"
|
||||
config_get default_mash_temp brewing default_mash_temp "67"
|
||||
config_get units brewing units "metric"
|
||||
|
||||
json_init_obj
|
||||
json_add_object "main"
|
||||
json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )"
|
||||
json_add_int "http_port" "$http_port"
|
||||
json_add_string "http_host" "$http_host"
|
||||
json_add_string "data_path" "$data_path"
|
||||
json_add_string "memory_limit" "$memory_limit"
|
||||
json_add_string "log_level" "$log_level"
|
||||
json_close_object
|
||||
|
||||
json_add_object "server"
|
||||
json_add_string "dns_name" "$dns_name"
|
||||
json_add_boolean "https_enabled" "$( [ "$https_enabled" = "1" ] && echo 1 || echo 0 )"
|
||||
json_add_string "cert_path" "$cert_path"
|
||||
json_add_string "key_path" "$key_path"
|
||||
json_close_object
|
||||
|
||||
json_add_object "brewing"
|
||||
json_add_int "default_boil_temp" "$default_boil_temp"
|
||||
json_add_int "default_mash_temp" "$default_mash_temp"
|
||||
json_add_string "units" "$units"
|
||||
json_close_object
|
||||
|
||||
json_close_obj
|
||||
}
|
||||
|
||||
# Set configuration value
|
||||
set_config() {
|
||||
local section="$1"
|
||||
local option="$2"
|
||||
local value="$3"
|
||||
|
||||
if [ -z "$section" ] || [ -z "$option" ]; then
|
||||
json_error "Missing section or option"
|
||||
return
|
||||
fi
|
||||
|
||||
uci set "${CONFIG}.${section}.${option}=${value}"
|
||||
uci commit "$CONFIG"
|
||||
|
||||
json_success "Configuration updated"
|
||||
}
|
||||
|
||||
# Start service
|
||||
start_service() {
|
||||
if lxc_running; then
|
||||
json_error "Service is already running"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! lxc_exists; then
|
||||
json_error "Container not installed. Run install first."
|
||||
return
|
||||
fi
|
||||
|
||||
/etc/init.d/picobrew start >/dev/null 2>&1 &
|
||||
|
||||
sleep 2
|
||||
if lxc_running; then
|
||||
json_success "Service started"
|
||||
else
|
||||
json_error "Failed to start service"
|
||||
fi
|
||||
}
|
||||
|
||||
# Stop service
|
||||
stop_service() {
|
||||
if ! lxc_running; then
|
||||
json_error "Service is not running"
|
||||
return
|
||||
fi
|
||||
|
||||
/etc/init.d/picobrew stop >/dev/null 2>&1
|
||||
|
||||
sleep 2
|
||||
if ! lxc_running; then
|
||||
json_success "Service stopped"
|
||||
else
|
||||
json_error "Failed to stop service"
|
||||
fi
|
||||
}
|
||||
|
||||
# Restart service
|
||||
restart_service() {
|
||||
/etc/init.d/picobrew restart >/dev/null 2>&1 &
|
||||
|
||||
sleep 3
|
||||
if lxc_running; then
|
||||
json_success "Service restarted"
|
||||
else
|
||||
json_error "Service restart failed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Install PicoBrew
|
||||
install() {
|
||||
if lxc_exists; then
|
||||
json_error "Already installed. Use update to refresh."
|
||||
return
|
||||
fi
|
||||
|
||||
# Run install in background
|
||||
/usr/sbin/picobrewctl install >/var/log/picobrew-install.log 2>&1 &
|
||||
|
||||
json_init_obj
|
||||
json_add_boolean "started" 1
|
||||
json_add_string "message" "Installation started in background"
|
||||
json_add_string "log_file" "/var/log/picobrew-install.log"
|
||||
json_close_obj
|
||||
}
|
||||
|
||||
# Uninstall PicoBrew
|
||||
uninstall() {
|
||||
/usr/sbin/picobrewctl uninstall >/dev/null 2>&1
|
||||
|
||||
if ! lxc_exists; then
|
||||
json_success "Uninstalled successfully"
|
||||
else
|
||||
json_error "Uninstall failed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Update PicoBrew
|
||||
update() {
|
||||
if ! [ -d "$REPO_PATH/.git" ]; then
|
||||
json_error "Not installed. Run install first."
|
||||
return
|
||||
fi
|
||||
|
||||
# Run update in background
|
||||
/usr/sbin/picobrewctl update >/var/log/picobrew-update.log 2>&1 &
|
||||
|
||||
json_init_obj
|
||||
json_add_boolean "started" 1
|
||||
json_add_string "message" "Update started in background"
|
||||
json_add_string "log_file" "/var/log/picobrew-update.log"
|
||||
json_close_obj
|
||||
}
|
||||
|
||||
# Get logs
|
||||
get_logs() {
|
||||
local lines="${1:-100}"
|
||||
local data_path
|
||||
|
||||
config_load "$CONFIG"
|
||||
config_get data_path main data_path "/srv/picobrew"
|
||||
|
||||
json_init_obj
|
||||
json_add_array "logs"
|
||||
|
||||
# Get container logs from data path
|
||||
if [ -d "$data_path/logs" ]; then
|
||||
local logfile
|
||||
for logfile in "$data_path/logs"/*.log; do
|
||||
[ -f "$logfile" ] || continue
|
||||
tail -n "$lines" "$logfile" 2>/dev/null | while IFS= read -r line; do
|
||||
json_add_string "" "$line"
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
# Also check install/update logs
|
||||
for logfile in /var/log/picobrew-install.log /var/log/picobrew-update.log; do
|
||||
[ -f "$logfile" ] || continue
|
||||
tail -n 50 "$logfile" 2>/dev/null | while IFS= read -r line; do
|
||||
json_add_string "" "$line"
|
||||
done
|
||||
done
|
||||
|
||||
json_close_array
|
||||
json_close_obj
|
||||
}
|
||||
|
||||
# Get brewing sessions (placeholder - depends on picobrew-server API)
|
||||
get_sessions() {
|
||||
local data_path
|
||||
config_load "$CONFIG"
|
||||
config_get data_path main data_path "/srv/picobrew"
|
||||
|
||||
json_init_obj
|
||||
json_add_array "sessions"
|
||||
|
||||
# List session files if they exist
|
||||
if [ -d "$data_path/sessions" ]; then
|
||||
for session in "$data_path/sessions"/*.json; do
|
||||
[ -f "$session" ] || continue
|
||||
local name=$(basename "$session" .json)
|
||||
json_add_object ""
|
||||
json_add_string "id" "$name"
|
||||
json_add_string "file" "$session"
|
||||
json_close_object
|
||||
done
|
||||
fi
|
||||
|
||||
json_close_array
|
||||
json_close_obj
|
||||
}
|
||||
|
||||
# Get recipes (placeholder - depends on picobrew-server API)
|
||||
get_recipes() {
|
||||
local data_path
|
||||
config_load "$CONFIG"
|
||||
config_get data_path main data_path "/srv/picobrew"
|
||||
|
||||
json_init_obj
|
||||
json_add_array "recipes"
|
||||
|
||||
# List recipe files if they exist
|
||||
if [ -d "$data_path/recipes" ]; then
|
||||
for recipe in "$data_path/recipes"/*.json; do
|
||||
[ -f "$recipe" ] || continue
|
||||
local name=$(basename "$recipe" .json)
|
||||
json_add_object ""
|
||||
json_add_string "id" "$name"
|
||||
json_add_string "file" "$recipe"
|
||||
json_close_object
|
||||
done
|
||||
fi
|
||||
|
||||
json_close_array
|
||||
json_close_obj
|
||||
}
|
||||
|
||||
# Check install progress
|
||||
get_install_progress() {
|
||||
local log_file="/var/log/picobrew-install.log"
|
||||
local status="unknown"
|
||||
local progress=0
|
||||
local message=""
|
||||
|
||||
if [ -f "$log_file" ]; then
|
||||
# Check for completion markers
|
||||
if grep -q "Installation complete" "$log_file" 2>/dev/null; then
|
||||
status="completed"
|
||||
progress=100
|
||||
message="Installation completed successfully"
|
||||
elif grep -q "ERROR" "$log_file" 2>/dev/null; then
|
||||
status="error"
|
||||
message=$(grep "ERROR" "$log_file" | tail -1)
|
||||
else
|
||||
status="running"
|
||||
# Estimate progress based on log content
|
||||
if grep -q "Extracting rootfs" "$log_file" 2>/dev/null; then
|
||||
progress=60
|
||||
message="Extracting container rootfs..."
|
||||
elif grep -q "Downloading Alpine" "$log_file" 2>/dev/null; then
|
||||
progress=40
|
||||
message="Downloading Alpine rootfs..."
|
||||
elif grep -q "Cloning" "$log_file" 2>/dev/null; then
|
||||
progress=20
|
||||
message="Cloning PicoBrew repository..."
|
||||
else
|
||||
progress=10
|
||||
message="Starting installation..."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
status="not_started"
|
||||
message="Installation has not been started"
|
||||
fi
|
||||
|
||||
# Check if process is still running
|
||||
if pgrep -f "picobrewctl install" >/dev/null 2>&1; then
|
||||
status="running"
|
||||
fi
|
||||
|
||||
json_init_obj
|
||||
json_add_string "status" "$status"
|
||||
json_add_int "progress" "$progress"
|
||||
json_add_string "message" "$message"
|
||||
json_close_obj
|
||||
}
|
||||
|
||||
# Main RPC handler
|
||||
case "$1" in
|
||||
list)
|
||||
cat <<-EOF
|
||||
{
|
||||
"get_status": {},
|
||||
"get_config": {},
|
||||
"set_config": {"section": "str", "option": "str", "value": "str"},
|
||||
"start": {},
|
||||
"stop": {},
|
||||
"restart": {},
|
||||
"install": {},
|
||||
"uninstall": {},
|
||||
"update": {},
|
||||
"get_logs": {"lines": 100},
|
||||
"get_sessions": {},
|
||||
"get_recipes": {},
|
||||
"get_install_progress": {}
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
get_status)
|
||||
get_status
|
||||
;;
|
||||
get_config)
|
||||
get_config
|
||||
;;
|
||||
set_config)
|
||||
read -r input
|
||||
section=$(echo "$input" | jsonfilter -e '@.section' 2>/dev/null)
|
||||
option=$(echo "$input" | jsonfilter -e '@.option' 2>/dev/null)
|
||||
value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null)
|
||||
set_config "$section" "$option" "$value"
|
||||
;;
|
||||
start)
|
||||
start_service
|
||||
;;
|
||||
stop)
|
||||
stop_service
|
||||
;;
|
||||
restart)
|
||||
restart_service
|
||||
;;
|
||||
install)
|
||||
install
|
||||
;;
|
||||
uninstall)
|
||||
uninstall
|
||||
;;
|
||||
update)
|
||||
update
|
||||
;;
|
||||
get_logs)
|
||||
read -r input
|
||||
lines=$(echo "$input" | jsonfilter -e '@.lines' 2>/dev/null)
|
||||
get_logs "${lines:-100}"
|
||||
;;
|
||||
get_sessions)
|
||||
get_sessions
|
||||
;;
|
||||
get_recipes)
|
||||
get_recipes
|
||||
;;
|
||||
get_install_progress)
|
||||
get_install_progress
|
||||
;;
|
||||
*)
|
||||
json_error "Unknown method: $2"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,29 @@
|
||||
{
|
||||
"admin/services/picobrew": {
|
||||
"title": "PicoBrew",
|
||||
"order": 85,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-picobrew"],
|
||||
"uci": {"picobrew": true}
|
||||
}
|
||||
},
|
||||
"admin/services/picobrew/dashboard": {
|
||||
"title": "Dashboard",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "picobrew/dashboard"
|
||||
}
|
||||
},
|
||||
"admin/services/picobrew/settings": {
|
||||
"title": "Settings",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "picobrew/settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"luci-app-picobrew": {
|
||||
"description": "Grant access to PicoBrew Server management",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.picobrew": ["get_status", "get_config", "get_logs", "get_sessions", "get_recipes", "get_install_progress"]
|
||||
},
|
||||
"uci": ["picobrew"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.picobrew": ["set_config", "start", "stop", "restart", "install", "uninstall", "update"]
|
||||
},
|
||||
"uci": ["picobrew"]
|
||||
}
|
||||
}
|
||||
}
|
||||
78
package/secubox/secubox-app-picobrew/Makefile
Normal file
78
package/secubox/secubox-app-picobrew/Makefile
Normal file
@ -0,0 +1,78 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
#
|
||||
# SecuBox PicoBrew Server - Homebrew brewing controller
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-picobrew
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_ARCH:=all
|
||||
|
||||
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/secubox-app-picobrew
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
PKGARCH:=all
|
||||
SUBMENU:=SecuBox Apps
|
||||
TITLE:=SecuBox PicoBrew Server
|
||||
DEPENDS:=+uci +libuci +jsonfilter +wget-ssl +tar +lxc +lxc-common +git
|
||||
endef
|
||||
|
||||
define Package/secubox-app-picobrew/description
|
||||
PicoBrew Server - Self-hosted brewing controller for PicoBrew devices
|
||||
|
||||
Features:
|
||||
- Control PicoBrew Zymatic, Z, Pico C, and Pico Pro devices
|
||||
- Recipe management and import from BeerSmith
|
||||
- Real-time brewing session monitoring
|
||||
- Temperature and step control
|
||||
- Session history and logging
|
||||
|
||||
Runs in LXC container with Python/Flask backend.
|
||||
Configure in /etc/config/picobrew.
|
||||
endef
|
||||
|
||||
define Package/secubox-app-picobrew/conffiles
|
||||
/etc/config/picobrew
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/secubox-app-picobrew/install
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./files/etc/config/picobrew $(1)/etc/config/picobrew
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/etc/init.d/picobrew $(1)/etc/init.d/picobrew
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) ./files/usr/sbin/picobrewctl $(1)/usr/sbin/picobrewctl
|
||||
endef
|
||||
|
||||
define Package/secubox-app-picobrew/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
echo ""
|
||||
echo "PicoBrew Server installed."
|
||||
echo ""
|
||||
echo "To install and start PicoBrew Server:"
|
||||
echo " picobrewctl install"
|
||||
echo " /etc/init.d/picobrew start"
|
||||
echo ""
|
||||
echo "Web interface: http://<router-ip>:8080"
|
||||
echo ""
|
||||
echo "Configure your PicoBrew device to connect to this server."
|
||||
echo ""
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,secubox-app-picobrew))
|
||||
@ -0,0 +1,18 @@
|
||||
config picobrew 'main'
|
||||
option enabled '0'
|
||||
option http_port '8080'
|
||||
option http_host '0.0.0.0'
|
||||
option data_path '/srv/picobrew'
|
||||
option memory_limit '512M'
|
||||
option log_level 'INFO'
|
||||
|
||||
config server 'server'
|
||||
option dns_name ''
|
||||
option https_enabled '0'
|
||||
option cert_path ''
|
||||
option key_path ''
|
||||
|
||||
config brewing 'brewing'
|
||||
option default_boil_temp '100'
|
||||
option default_mash_temp '67'
|
||||
option units 'metric'
|
||||
@ -0,0 +1,45 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# SecuBox PicoBrew Server - Homebrew brewing controller
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
|
||||
START=95
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
PROG=/usr/sbin/picobrewctl
|
||||
CONFIG=picobrew
|
||||
|
||||
start_service() {
|
||||
local enabled
|
||||
config_load "$CONFIG"
|
||||
config_get enabled main enabled '0'
|
||||
|
||||
[ "$enabled" = "1" ] || {
|
||||
echo "PicoBrew is disabled. Enable with: uci set picobrew.main.enabled=1"
|
||||
return 0
|
||||
}
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command "$PROG" service-run
|
||||
procd_set_param respawn 3600 5 5
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
"$PROG" service-stop
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONFIG"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
status() {
|
||||
"$PROG" status
|
||||
}
|
||||
427
package/secubox/secubox-app-picobrew/files/usr/sbin/picobrewctl
Normal file
427
package/secubox/secubox-app-picobrew/files/usr/sbin/picobrewctl
Normal file
@ -0,0 +1,427 @@
|
||||
#!/bin/sh
|
||||
# SecuBox PicoBrew Server Controller
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
#
|
||||
# Manages PicoBrew Server in LXC container
|
||||
|
||||
CONFIG="picobrew"
|
||||
LXC_NAME="picobrew"
|
||||
|
||||
# Paths
|
||||
LXC_PATH="/srv/lxc"
|
||||
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
|
||||
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
|
||||
REPO_URL="https://github.com/CyberMind-FR/picobrew-server.git"
|
||||
REPO_PATH="/srv/picobrew/app"
|
||||
|
||||
# Logging
|
||||
log_info() { echo "[INFO] $*"; logger -t picobrew "$*"; }
|
||||
log_error() { echo "[ERROR] $*" >&2; logger -t picobrew -p err "$*"; }
|
||||
log_debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"; }
|
||||
|
||||
# Helpers
|
||||
require_root() {
|
||||
[ "$(id -u)" -eq 0 ] || {
|
||||
log_error "This command requires root privileges"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
has_lxc() { command -v lxc-start >/dev/null 2>&1; }
|
||||
has_git() { command -v git >/dev/null 2>&1; }
|
||||
|
||||
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
|
||||
|
||||
uci_get() { uci -q get ${CONFIG}.$1; }
|
||||
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
|
||||
|
||||
# Load configuration
|
||||
load_config() {
|
||||
http_port="$(uci_get main.http_port)" || http_port="8080"
|
||||
http_host="$(uci_get main.http_host)" || http_host="0.0.0.0"
|
||||
data_path="$(uci_get main.data_path)" || data_path="/srv/picobrew"
|
||||
memory_limit="$(uci_get main.memory_limit)" || memory_limit="512M"
|
||||
log_level="$(uci_get main.log_level)" || log_level="INFO"
|
||||
dns_name="$(uci_get server.dns_name)" || dns_name=""
|
||||
https_enabled="$(uci_get server.https_enabled)" || https_enabled="0"
|
||||
units="$(uci_get brewing.units)" || units="metric"
|
||||
|
||||
ensure_dir "$data_path"
|
||||
ensure_dir "$data_path/recipes"
|
||||
ensure_dir "$data_path/sessions"
|
||||
ensure_dir "$data_path/logs"
|
||||
}
|
||||
|
||||
# Usage
|
||||
usage() {
|
||||
cat <<EOF
|
||||
SecuBox PicoBrew Server Controller
|
||||
|
||||
Usage: $(basename $0) <command> [options]
|
||||
|
||||
Commands:
|
||||
install Download and install PicoBrew Server
|
||||
uninstall Remove PicoBrew Server container
|
||||
update Update PicoBrew Server to latest version
|
||||
status Show service status
|
||||
logs Show container logs
|
||||
shell Open shell in container
|
||||
|
||||
service-run Start service (used by init)
|
||||
service-stop Stop service (used by init)
|
||||
|
||||
Configuration:
|
||||
/etc/config/picobrew
|
||||
|
||||
Data directory:
|
||||
/srv/picobrew
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
lxc_check_prereqs() {
|
||||
if ! has_lxc; then
|
||||
log_error "LXC not installed. Install with: opkg install lxc lxc-common"
|
||||
return 1
|
||||
fi
|
||||
if ! has_git; then
|
||||
log_error "Git not installed. Install with: opkg install git"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Clone or update repo
|
||||
repo_clone() {
|
||||
load_config
|
||||
|
||||
if [ -d "$REPO_PATH/.git" ]; then
|
||||
log_info "Updating PicoBrew Server repository..."
|
||||
cd "$REPO_PATH" && git pull
|
||||
else
|
||||
log_info "Cloning PicoBrew Server repository..."
|
||||
ensure_dir "$(dirname "$REPO_PATH")"
|
||||
git clone "$REPO_URL" "$REPO_PATH"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create Python LXC rootfs from Alpine
|
||||
lxc_create_rootfs() {
|
||||
local rootfs="$LXC_ROOTFS"
|
||||
local arch=$(uname -m)
|
||||
|
||||
log_info "Creating Alpine rootfs for PicoBrew..."
|
||||
|
||||
ensure_dir "$rootfs"
|
||||
|
||||
# Use Alpine mini rootfs
|
||||
local alpine_version="3.19"
|
||||
case "$arch" in
|
||||
x86_64) alpine_arch="x86_64" ;;
|
||||
aarch64) alpine_arch="aarch64" ;;
|
||||
armv7l) alpine_arch="armv7" ;;
|
||||
*) log_error "Unsupported architecture: $arch"; return 1 ;;
|
||||
esac
|
||||
|
||||
local alpine_url="https://dl-cdn.alpinelinux.org/alpine/v${alpine_version}/releases/${alpine_arch}/alpine-minirootfs-${alpine_version}.0-${alpine_arch}.tar.gz"
|
||||
local tmpfile="/tmp/alpine-rootfs.tar.gz"
|
||||
|
||||
log_info "Downloading Alpine rootfs..."
|
||||
wget -q -O "$tmpfile" "$alpine_url" || {
|
||||
log_error "Failed to download Alpine rootfs"
|
||||
return 1
|
||||
}
|
||||
|
||||
log_info "Extracting rootfs..."
|
||||
tar -xzf "$tmpfile" -C "$rootfs" || {
|
||||
log_error "Failed to extract rootfs"
|
||||
return 1
|
||||
}
|
||||
rm -f "$tmpfile"
|
||||
|
||||
# Setup resolv.conf
|
||||
cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || \
|
||||
echo "nameserver 1.1.1.1" > "$rootfs/etc/resolv.conf"
|
||||
|
||||
# Create startup script
|
||||
cat > "$rootfs/opt/start-picobrew.sh" << 'STARTUP'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Install Python and dependencies on first run
|
||||
if [ ! -f /opt/.installed ]; then
|
||||
echo "Installing Python and dependencies..."
|
||||
apk update
|
||||
apk add --no-cache python3 py3-pip py3-flask py3-requests py3-pyyaml git
|
||||
|
||||
# Install PicoBrew dependencies
|
||||
if [ -d /app ]; then
|
||||
cd /app
|
||||
pip3 install --break-system-packages -r requirements.txt 2>/dev/null || \
|
||||
pip3 install -r requirements.txt 2>/dev/null || true
|
||||
fi
|
||||
|
||||
touch /opt/.installed
|
||||
fi
|
||||
|
||||
# Start PicoBrew Server
|
||||
cd /app
|
||||
export FLASK_APP=app.py
|
||||
export FLASK_ENV=production
|
||||
export PICOBREW_HOST="${PICOBREW_HOST:-0.0.0.0}"
|
||||
export PICOBREW_PORT="${PICOBREW_PORT:-8080}"
|
||||
export PICOBREW_LOG_LEVEL="${PICOBREW_LOG_LEVEL:-INFO}"
|
||||
|
||||
echo "Starting PicoBrew Server on ${PICOBREW_HOST}:${PICOBREW_PORT}..."
|
||||
exec python3 -m flask run --host="$PICOBREW_HOST" --port="$PICOBREW_PORT"
|
||||
STARTUP
|
||||
chmod +x "$rootfs/opt/start-picobrew.sh"
|
||||
|
||||
log_info "Rootfs created successfully"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Create LXC config
|
||||
lxc_create_config() {
|
||||
load_config
|
||||
|
||||
ensure_dir "$(dirname "$LXC_CONFIG")"
|
||||
|
||||
# Convert memory limit to bytes
|
||||
local mem_bytes
|
||||
case "$memory_limit" in
|
||||
*G|*g) mem_bytes=$((${memory_limit%[Gg]} * 1024 * 1024 * 1024)) ;;
|
||||
*M|*m) mem_bytes=$((${memory_limit%[Mm]} * 1024 * 1024)) ;;
|
||||
*K|*k) mem_bytes=$((${memory_limit%[Kk]} * 1024)) ;;
|
||||
*) mem_bytes="$memory_limit" ;;
|
||||
esac
|
||||
|
||||
cat > "$LXC_CONFIG" << EOF
|
||||
# PicoBrew Server LXC Configuration
|
||||
lxc.uts.name = $LXC_NAME
|
||||
lxc.rootfs.path = dir:$LXC_ROOTFS
|
||||
lxc.arch = $(uname -m)
|
||||
|
||||
# Network: use host network for device discovery
|
||||
lxc.net.0.type = none
|
||||
|
||||
# Mount points
|
||||
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
|
||||
lxc.mount.entry = $REPO_PATH app none bind,create=dir 0 0
|
||||
lxc.mount.entry = $data_path/recipes app/recipes none bind,create=dir 0 0
|
||||
lxc.mount.entry = $data_path/sessions app/sessions none bind,create=dir 0 0
|
||||
lxc.mount.entry = $data_path/logs logs none bind,create=dir 0 0
|
||||
|
||||
# Environment
|
||||
lxc.environment = PICOBREW_HOST=$http_host
|
||||
lxc.environment = PICOBREW_PORT=$http_port
|
||||
lxc.environment = PICOBREW_LOG_LEVEL=$log_level
|
||||
lxc.environment = PICOBREW_UNITS=$units
|
||||
|
||||
# Security
|
||||
lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time sys_rawio
|
||||
|
||||
# Resource limits
|
||||
lxc.cgroup.memory.limit_in_bytes = $mem_bytes
|
||||
|
||||
# Init command
|
||||
lxc.init.cmd = /opt/start-picobrew.sh
|
||||
EOF
|
||||
|
||||
log_info "LXC config created"
|
||||
}
|
||||
|
||||
# Container control
|
||||
lxc_running() {
|
||||
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
|
||||
}
|
||||
|
||||
lxc_exists() {
|
||||
[ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ]
|
||||
}
|
||||
|
||||
lxc_stop() {
|
||||
if lxc_running; then
|
||||
log_info "Stopping PicoBrew container..."
|
||||
lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true
|
||||
sleep 2
|
||||
fi
|
||||
}
|
||||
|
||||
lxc_run() {
|
||||
load_config
|
||||
lxc_stop
|
||||
|
||||
if ! lxc_exists; then
|
||||
log_error "Container not installed. Run: picobrewctl install"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Regenerate config in case settings changed
|
||||
lxc_create_config
|
||||
|
||||
log_info "Starting PicoBrew container..."
|
||||
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
|
||||
}
|
||||
|
||||
# Commands
|
||||
cmd_install() {
|
||||
require_root
|
||||
load_config
|
||||
|
||||
log_info "Installing PicoBrew Server..."
|
||||
|
||||
lxc_check_prereqs || exit 1
|
||||
|
||||
# Clone repository
|
||||
repo_clone || exit 1
|
||||
|
||||
# Create container
|
||||
if ! lxc_exists; then
|
||||
lxc_create_rootfs || exit 1
|
||||
fi
|
||||
|
||||
lxc_create_config || exit 1
|
||||
|
||||
# Enable service
|
||||
uci_set main.enabled '1'
|
||||
/etc/init.d/picobrew enable 2>/dev/null || true
|
||||
|
||||
log_info "Installation complete!"
|
||||
log_info ""
|
||||
log_info "Start with: /etc/init.d/picobrew start"
|
||||
log_info "Web interface: http://<router-ip>:$http_port"
|
||||
}
|
||||
|
||||
cmd_uninstall() {
|
||||
require_root
|
||||
|
||||
log_info "Uninstalling PicoBrew Server..."
|
||||
|
||||
# Stop service
|
||||
/etc/init.d/picobrew stop 2>/dev/null || true
|
||||
/etc/init.d/picobrew disable 2>/dev/null || true
|
||||
|
||||
lxc_stop
|
||||
|
||||
# Remove container (keep data)
|
||||
if [ -d "$LXC_PATH/$LXC_NAME" ]; then
|
||||
rm -rf "$LXC_PATH/$LXC_NAME"
|
||||
log_info "Container removed"
|
||||
fi
|
||||
|
||||
uci_set main.enabled '0'
|
||||
|
||||
log_info "PicoBrew Server uninstalled"
|
||||
log_info "Data preserved in: $(uci_get main.data_path)"
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
require_root
|
||||
load_config
|
||||
|
||||
log_info "Updating PicoBrew Server..."
|
||||
|
||||
# Update repo
|
||||
repo_clone || exit 1
|
||||
|
||||
# Recreate container to get fresh dependencies
|
||||
lxc_stop
|
||||
if [ -d "$LXC_ROOTFS" ]; then
|
||||
rm -rf "$LXC_ROOTFS"
|
||||
fi
|
||||
lxc_create_rootfs || exit 1
|
||||
|
||||
# Restart if was running
|
||||
if [ "$(uci_get main.enabled)" = "1" ]; then
|
||||
/etc/init.d/picobrew restart
|
||||
fi
|
||||
|
||||
log_info "Update complete!"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
load_config
|
||||
|
||||
local enabled="$(uci_get main.enabled)"
|
||||
local running="false"
|
||||
local uptime=""
|
||||
|
||||
if lxc_running; then
|
||||
running="true"
|
||||
uptime=$(lxc-info -n "$LXC_NAME" 2>/dev/null | grep -i "cpu use" | head -1)
|
||||
fi
|
||||
|
||||
cat << EOF
|
||||
PicoBrew Server Status
|
||||
=====================
|
||||
Enabled: $([ "$enabled" = "1" ] && echo "yes" || echo "no")
|
||||
Running: $([ "$running" = "true" ] && echo "yes" || echo "no")
|
||||
HTTP Port: $http_port
|
||||
Data Path: $data_path
|
||||
Memory: $memory_limit
|
||||
|
||||
Container: $LXC_NAME
|
||||
Rootfs: $LXC_ROOTFS
|
||||
Config: $LXC_CONFIG
|
||||
|
||||
EOF
|
||||
|
||||
if [ "$running" = "true" ]; then
|
||||
echo "Web interface: http://$(uci -q get network.lan.ipaddr || echo "localhost"):$http_port"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_logs() {
|
||||
load_config
|
||||
|
||||
if [ -d "$data_path/logs" ]; then
|
||||
if [ -n "$(ls -A "$data_path/logs" 2>/dev/null)" ]; then
|
||||
tail -f "$data_path/logs"/*.log 2>/dev/null || \
|
||||
cat "$data_path/logs"/*.log 2>/dev/null || \
|
||||
echo "No logs found"
|
||||
else
|
||||
echo "No logs yet"
|
||||
fi
|
||||
else
|
||||
echo "Log directory not found"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_shell() {
|
||||
require_root
|
||||
|
||||
if ! lxc_running; then
|
||||
log_error "Container not running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
lxc-attach -n "$LXC_NAME" -- /bin/sh
|
||||
}
|
||||
|
||||
cmd_service_run() {
|
||||
require_root
|
||||
load_config
|
||||
|
||||
lxc_check_prereqs || exit 1
|
||||
lxc_run
|
||||
}
|
||||
|
||||
cmd_service_stop() {
|
||||
require_root
|
||||
lxc_stop
|
||||
}
|
||||
|
||||
# Main
|
||||
case "${1:-}" in
|
||||
install) shift; cmd_install "$@" ;;
|
||||
uninstall) shift; cmd_uninstall "$@" ;;
|
||||
update) shift; cmd_update "$@" ;;
|
||||
status) shift; cmd_status "$@" ;;
|
||||
logs) shift; cmd_logs "$@" ;;
|
||||
shell) shift; cmd_shell "$@" ;;
|
||||
service-run) shift; cmd_service_run "$@" ;;
|
||||
service-stop) shift; cmd_service_stop "$@" ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
Loading…
Reference in New Issue
Block a user