feat(luci-app-reporter): Add LuCI frontend for Report Generator
KISS-themed dashboard for SecuBox Report Generator: - Status overview with report counts and schedule status - Quick action cards for dev/services/all reports - Generate and Send buttons with email support - Reports list with view/delete actions - Schedule configuration (daily/weekly/off) - Email configuration status and test button RPCD Methods: - status: Get generator status and report counts - list_reports: List all generated reports with metadata - generate/send: Create reports (optionally send via email) - schedule: Configure cron schedules - delete_report: Remove report files - test_email: Send test email Menu: SecuBox → System → Report Generator Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5d316e7d72
commit
1479db43ad
33
package/secubox/luci-app-reporter/Makefile
Normal file
33
package/secubox/luci-app-reporter/Makefile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# LuCI App Reporter - Web UI for SecuBox Report Generator
|
||||||
|
# Copyright (C) 2025-2026 CyberMind.fr
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=luci-app-reporter
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
PKG_LICENSE:=MIT
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI Reporter Dashboard
|
||||||
|
LUCI_DEPENDS:=+secubox-app-reporter +luci-base
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
define Package/luci-app-reporter/install
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/reporter
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/reporter/*.js $(1)/www/luci-static/resources/view/reporter/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||||
|
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.reporter $(1)/usr/libexec/rpcd/luci.reporter
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,luci-app-reporter))
|
||||||
@ -0,0 +1,448 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require rpc';
|
||||||
|
'require ui';
|
||||||
|
'require poll';
|
||||||
|
'require dom';
|
||||||
|
'require secubox/kiss-theme';
|
||||||
|
|
||||||
|
var callGetStatus = rpc.declare({
|
||||||
|
object: 'luci.reporter',
|
||||||
|
method: 'status',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callListReports = rpc.declare({
|
||||||
|
object: 'luci.reporter',
|
||||||
|
method: 'list_reports',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGenerate = rpc.declare({
|
||||||
|
object: 'luci.reporter',
|
||||||
|
method: 'generate',
|
||||||
|
params: ['type'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callSend = rpc.declare({
|
||||||
|
object: 'luci.reporter',
|
||||||
|
method: 'send',
|
||||||
|
params: ['type'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callDeleteReport = rpc.declare({
|
||||||
|
object: 'luci.reporter',
|
||||||
|
method: 'delete_report',
|
||||||
|
params: ['filename'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callTestEmail = rpc.declare({
|
||||||
|
object: 'luci.reporter',
|
||||||
|
method: 'test_email',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callSchedule = rpc.declare({
|
||||||
|
object: 'luci.reporter',
|
||||||
|
method: 'schedule',
|
||||||
|
params: ['type', 'frequency'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
callGetStatus(),
|
||||||
|
callListReports()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime: function(timestamp) {
|
||||||
|
if (!timestamp || timestamp === 0) return 'Never';
|
||||||
|
var d = new Date(timestamp * 1000);
|
||||||
|
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString();
|
||||||
|
},
|
||||||
|
|
||||||
|
formatSize: function(bytes) {
|
||||||
|
if (bytes < 1024) return bytes + ' B';
|
||||||
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||||
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||||
|
},
|
||||||
|
|
||||||
|
renderStats: function(status) {
|
||||||
|
var c = KissTheme.colors;
|
||||||
|
var emailConfigured = status.email && status.email.configured;
|
||||||
|
|
||||||
|
return [
|
||||||
|
KissTheme.stat(status.report_count || 0, 'Reports', c.purple),
|
||||||
|
KissTheme.stat(
|
||||||
|
status.schedules ? (status.schedules.dev !== 'off' ? 'ON' : 'OFF') : 'OFF',
|
||||||
|
'Dev Schedule',
|
||||||
|
status.schedules && status.schedules.dev !== 'off' ? c.green : c.muted
|
||||||
|
),
|
||||||
|
KissTheme.stat(
|
||||||
|
status.schedules ? (status.schedules.services !== 'off' ? 'ON' : 'OFF') : 'OFF',
|
||||||
|
'Services Schedule',
|
||||||
|
status.schedules && status.schedules.services !== 'off' ? c.green : c.muted
|
||||||
|
),
|
||||||
|
KissTheme.stat(
|
||||||
|
emailConfigured ? 'OK' : 'N/A',
|
||||||
|
'Email',
|
||||||
|
emailConfigured ? c.green : c.muted
|
||||||
|
)
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderQuickActions: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return E('div', { 'class': 'kiss-grid kiss-grid-3', 'style': 'margin-bottom: 24px;' }, [
|
||||||
|
// Development Report
|
||||||
|
E('div', { 'class': 'kiss-card' }, [
|
||||||
|
E('div', { 'style': 'padding: 20px; text-align: center;' }, [
|
||||||
|
E('div', { 'style': 'font-size: 32px; margin-bottom: 12px;' }, '📊'),
|
||||||
|
E('h3', { 'style': 'margin: 0 0 8px 0; color: var(--kiss-text);' }, 'Development Status'),
|
||||||
|
E('p', { 'style': 'color: var(--kiss-muted); font-size: 12px; margin: 0 0 16px 0;' },
|
||||||
|
'Roadmap progress, HISTORY.md, WIP items'),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 8px; justify-content: center;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'kiss-btn kiss-btn-purple',
|
||||||
|
'click': function() { self.handleGenerate('dev'); }
|
||||||
|
}, 'Generate'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'kiss-btn',
|
||||||
|
'click': function() { self.handleSend('dev'); }
|
||||||
|
}, 'Send')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Services Report
|
||||||
|
E('div', { 'class': 'kiss-card' }, [
|
||||||
|
E('div', { 'style': 'padding: 20px; text-align: center;' }, [
|
||||||
|
E('div', { 'style': 'font-size: 32px; margin-bottom: 12px;' }, '🌐'),
|
||||||
|
E('h3', { 'style': 'margin: 0 0 8px 0; color: var(--kiss-text);' }, 'Services Distribution'),
|
||||||
|
E('p', { 'style': 'color: var(--kiss-muted); font-size: 12px; margin: 0 0 16px 0;' },
|
||||||
|
'Tor services, DNS/SSL vhosts, mesh services'),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 8px; justify-content: center;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'kiss-btn kiss-btn-cyan',
|
||||||
|
'click': function() { self.handleGenerate('services'); }
|
||||||
|
}, 'Generate'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'kiss-btn',
|
||||||
|
'click': function() { self.handleSend('services'); }
|
||||||
|
}, 'Send')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// All Reports
|
||||||
|
E('div', { 'class': 'kiss-card' }, [
|
||||||
|
E('div', { 'style': 'padding: 20px; text-align: center;' }, [
|
||||||
|
E('div', { 'style': 'font-size: 32px; margin-bottom: 12px;' }, '📦'),
|
||||||
|
E('h3', { 'style': 'margin: 0 0 8px 0; color: var(--kiss-text);' }, 'Full Report'),
|
||||||
|
E('p', { 'style': 'color: var(--kiss-muted); font-size: 12px; margin: 0 0 16px 0;' },
|
||||||
|
'Generate both reports at once'),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 8px; justify-content: center;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'kiss-btn kiss-btn-green',
|
||||||
|
'click': function() { self.handleGenerate('all'); }
|
||||||
|
}, 'Generate All'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'kiss-btn',
|
||||||
|
'click': function() { self.handleSend('all'); }
|
||||||
|
}, 'Send All')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderReportsList: function(reports) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!reports || reports.length === 0) {
|
||||||
|
return E('div', { 'style': 'text-align: center; padding: 32px; color: var(--kiss-muted);' }, [
|
||||||
|
E('div', { 'style': 'font-size: 48px; margin-bottom: 12px;' }, '📄'),
|
||||||
|
E('p', {}, 'No reports generated yet'),
|
||||||
|
E('p', { 'style': 'font-size: 12px;' }, 'Use the buttons above to generate your first report')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by mtime descending
|
||||||
|
reports.sort(function(a, b) { return b.mtime - a.mtime; });
|
||||||
|
|
||||||
|
return E('table', { 'class': 'kiss-table' }, [
|
||||||
|
E('thead', {}, E('tr', {}, [
|
||||||
|
E('th', {}, 'Report'),
|
||||||
|
E('th', {}, 'Type'),
|
||||||
|
E('th', {}, 'Size'),
|
||||||
|
E('th', {}, 'Generated'),
|
||||||
|
E('th', { 'style': 'width: 150px;' }, 'Actions')
|
||||||
|
])),
|
||||||
|
E('tbody', { 'id': 'reports-body' }, reports.map(function(r) {
|
||||||
|
var typeColor = r.type === 'dev' ? 'purple' : (r.type === 'services' ? 'cyan' : 'muted');
|
||||||
|
return E('tr', {}, [
|
||||||
|
E('td', { 'style': 'font-family: monospace;' }, r.filename),
|
||||||
|
E('td', {}, KissTheme.badge(r.type.toUpperCase(), typeColor)),
|
||||||
|
E('td', { 'style': 'color: var(--kiss-muted);' }, self.formatSize(r.size)),
|
||||||
|
E('td', { 'style': 'color: var(--kiss-muted); font-size: 12px;' }, self.formatTime(r.mtime)),
|
||||||
|
E('td', {}, [
|
||||||
|
E('a', {
|
||||||
|
'href': r.url,
|
||||||
|
'target': '_blank',
|
||||||
|
'class': 'kiss-btn',
|
||||||
|
'style': 'padding: 4px 12px; font-size: 11px; margin-right: 8px; text-decoration: none;'
|
||||||
|
}, 'View'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'kiss-btn kiss-btn-red',
|
||||||
|
'style': 'padding: 4px 12px; font-size: 11px;',
|
||||||
|
'click': function() { self.handleDelete(r.filename); }
|
||||||
|
}, 'Delete')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderEmailStatus: function(status) {
|
||||||
|
var self = this;
|
||||||
|
var email = status.email || {};
|
||||||
|
|
||||||
|
if (!email.configured) {
|
||||||
|
return E('div', { 'style': 'text-align: center; padding: 24px;' }, [
|
||||||
|
E('p', { 'style': 'color: var(--kiss-muted); margin-bottom: 12px;' },
|
||||||
|
'Email not configured. Edit /etc/config/secubox-reporter to add SMTP settings.'),
|
||||||
|
E('code', { 'style': 'font-size: 11px; color: var(--kiss-cyan);' },
|
||||||
|
'uci set secubox-reporter.email.smtp_server="smtp.example.com"')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', {}, [
|
||||||
|
E('div', { 'style': 'display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-bottom: 16px;' }, [
|
||||||
|
E('div', {}, [
|
||||||
|
E('span', { 'style': 'color: var(--kiss-muted); font-size: 11px;' }, 'SMTP Server'),
|
||||||
|
E('div', { 'style': 'font-family: monospace;' }, email.smtp_server || '-')
|
||||||
|
]),
|
||||||
|
E('div', {}, [
|
||||||
|
E('span', { 'style': 'color: var(--kiss-muted); font-size: 11px;' }, 'Recipient'),
|
||||||
|
E('div', { 'style': 'font-family: monospace;' }, email.recipient || '-')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('button', {
|
||||||
|
'class': 'kiss-btn',
|
||||||
|
'click': function() { self.handleTestEmail(); }
|
||||||
|
}, 'Send Test Email')
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSchedules: function(status) {
|
||||||
|
var self = this;
|
||||||
|
var schedules = status.schedules || {};
|
||||||
|
|
||||||
|
return E('div', { 'style': 'display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;' }, [
|
||||||
|
// Dev schedule
|
||||||
|
E('div', {}, [
|
||||||
|
E('div', { 'style': 'margin-bottom: 8px;' }, [
|
||||||
|
E('span', { 'style': 'font-weight: 600;' }, 'Development Report'),
|
||||||
|
E('span', { 'style': 'margin-left: 8px;' },
|
||||||
|
KissTheme.badge(schedules.dev === 'off' ? 'OFF' : schedules.dev.toUpperCase(),
|
||||||
|
schedules.dev === 'off' ? 'muted' : 'green'))
|
||||||
|
]),
|
||||||
|
E('select', {
|
||||||
|
'class': 'kiss-select',
|
||||||
|
'style': 'width: 100%;',
|
||||||
|
'id': 'dev-schedule',
|
||||||
|
'change': function(ev) { self.handleScheduleChange('dev', ev.target.value); }
|
||||||
|
}, [
|
||||||
|
E('option', { 'value': 'off', 'selected': schedules.dev === 'off' }, 'Off'),
|
||||||
|
E('option', { 'value': 'daily', 'selected': schedules.dev === 'daily' }, 'Daily (6 AM)'),
|
||||||
|
E('option', { 'value': 'weekly', 'selected': schedules.dev === 'weekly' }, 'Weekly (Monday)')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Services schedule
|
||||||
|
E('div', {}, [
|
||||||
|
E('div', { 'style': 'margin-bottom: 8px;' }, [
|
||||||
|
E('span', { 'style': 'font-weight: 600;' }, 'Services Report'),
|
||||||
|
E('span', { 'style': 'margin-left: 8px;' },
|
||||||
|
KissTheme.badge(schedules.services === 'off' ? 'OFF' : schedules.services.toUpperCase(),
|
||||||
|
schedules.services === 'off' ? 'muted' : 'green'))
|
||||||
|
]),
|
||||||
|
E('select', {
|
||||||
|
'class': 'kiss-select',
|
||||||
|
'style': 'width: 100%;',
|
||||||
|
'id': 'services-schedule',
|
||||||
|
'change': function(ev) { self.handleScheduleChange('services', ev.target.value); }
|
||||||
|
}, [
|
||||||
|
E('option', { 'value': 'off', 'selected': schedules.services === 'off' }, 'Off'),
|
||||||
|
E('option', { 'value': 'daily', 'selected': schedules.services === 'daily' }, 'Daily (6 AM)'),
|
||||||
|
E('option', { 'value': 'weekly', 'selected': schedules.services === 'weekly' }, 'Weekly (Monday)')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleGenerate: function(type) {
|
||||||
|
var self = this;
|
||||||
|
var typeLabel = type === 'all' ? 'all reports' : (type + ' report');
|
||||||
|
|
||||||
|
ui.showModal('Generating Report', [
|
||||||
|
E('p', { 'class': 'spinning' }, 'Generating ' + typeLabel + '...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return callGenerate(type).then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, 'Report generated successfully'), 'success');
|
||||||
|
// Refresh reports list
|
||||||
|
return self.refreshReports();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, 'Failed: ' + (result.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSend: function(type) {
|
||||||
|
var self = this;
|
||||||
|
var typeLabel = type === 'all' ? 'all reports' : (type + ' report');
|
||||||
|
|
||||||
|
ui.showModal('Sending Report', [
|
||||||
|
E('p', { 'class': 'spinning' }, 'Generating and sending ' + typeLabel + '...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return callSend(type).then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, 'Report sent successfully'), 'success');
|
||||||
|
return self.refreshReports();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, 'Failed: ' + (result.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDelete: function(filename) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!confirm('Delete report ' + filename + '?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return callDeleteReport(filename).then(function(result) {
|
||||||
|
if (result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, 'Report deleted'), 'success');
|
||||||
|
return self.refreshReports();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, 'Failed: ' + (result.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTestEmail: function() {
|
||||||
|
ui.showModal('Testing Email', [
|
||||||
|
E('p', { 'class': 'spinning' }, 'Sending test email...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return callTestEmail().then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, result.message || 'Test email sent'), 'success');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, 'Failed: ' + (result.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleScheduleChange: function(type, frequency) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return callSchedule(type, frequency).then(function(result) {
|
||||||
|
if (result.success) {
|
||||||
|
ui.addNotification(null, E('p', {},
|
||||||
|
type.charAt(0).toUpperCase() + type.slice(1) + ' schedule set to ' + frequency), 'success');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, 'Failed: ' + (result.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshReports: function() {
|
||||||
|
var self = this;
|
||||||
|
return Promise.all([
|
||||||
|
callGetStatus(),
|
||||||
|
callListReports()
|
||||||
|
]).then(function(data) {
|
||||||
|
self.updateDashboard(data[0], data[1]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateDashboard: function(status, reports) {
|
||||||
|
// Update stats
|
||||||
|
var statsEl = document.getElementById('reporter-stats');
|
||||||
|
if (statsEl) {
|
||||||
|
dom.content(statsEl, this.renderStats(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update reports list
|
||||||
|
var reportsEl = document.getElementById('reports-list');
|
||||||
|
if (reportsEl) {
|
||||||
|
dom.content(reportsEl, this.renderReportsList(reports.reports || []));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var self = this;
|
||||||
|
var status = data[0] || {};
|
||||||
|
var reports = data[1] || {};
|
||||||
|
|
||||||
|
var content = [
|
||||||
|
// Header
|
||||||
|
E('div', { 'style': 'margin-bottom: 24px;' }, [
|
||||||
|
E('div', { 'style': 'display: flex; align-items: center; gap: 16px; flex-wrap: wrap;' }, [
|
||||||
|
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'Report Generator'),
|
||||||
|
KissTheme.badge(status.enabled ? 'ENABLED' : 'DISABLED', status.enabled ? 'green' : 'muted')
|
||||||
|
]),
|
||||||
|
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' },
|
||||||
|
'Generate and distribute SecuBox status reports')
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Stats row
|
||||||
|
E('div', { 'class': 'kiss-grid kiss-grid-4', 'id': 'reporter-stats', 'style': 'margin: 20px 0;' },
|
||||||
|
this.renderStats(status)),
|
||||||
|
|
||||||
|
// Quick Actions
|
||||||
|
this.renderQuickActions(),
|
||||||
|
|
||||||
|
// Two column layout
|
||||||
|
E('div', { 'class': 'kiss-grid kiss-grid-2' }, [
|
||||||
|
// Schedules
|
||||||
|
KissTheme.card('Scheduled Reports', this.renderSchedules(status)),
|
||||||
|
// Email Status
|
||||||
|
KissTheme.card('Email Configuration', this.renderEmailStatus(status))
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Reports List
|
||||||
|
KissTheme.card('Generated Reports', E('div', { 'id': 'reports-list' },
|
||||||
|
this.renderReportsList(reports.reports || []))),
|
||||||
|
|
||||||
|
// Last generated info
|
||||||
|
E('div', { 'style': 'margin-top: 16px; color: var(--kiss-muted); font-size: 12px;' }, [
|
||||||
|
status.last_reports && status.last_reports.dev_time > 0 ?
|
||||||
|
E('span', {}, 'Last dev report: ' + this.formatTime(status.last_reports.dev_time) + ' | ') : '',
|
||||||
|
status.last_reports && status.last_reports.services_time > 0 ?
|
||||||
|
E('span', {}, 'Last services report: ' + this.formatTime(status.last_reports.services_time)) : ''
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
return KissTheme.wrap(content, 'admin/secubox/system/reporter');
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
523
package/secubox/luci-app-reporter/root/usr/libexec/rpcd/luci.reporter
Executable file
523
package/secubox/luci-app-reporter/root/usr/libexec/rpcd/luci.reporter
Executable file
@ -0,0 +1,523 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# RPCD backend for SecuBox Report Generator
|
||||||
|
# Provides LuCI integration for report generation and management
|
||||||
|
|
||||||
|
. /lib/functions.sh
|
||||||
|
. /usr/share/libubox/jshn.sh
|
||||||
|
|
||||||
|
CONFIG_NAME="secubox-reporter"
|
||||||
|
OUTPUT_DIR="/www/reports"
|
||||||
|
REPORTCTL="/usr/sbin/secubox-reportctl"
|
||||||
|
|
||||||
|
# Method: list
|
||||||
|
method_list() {
|
||||||
|
json_init
|
||||||
|
json_add_object "status"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "list_reports"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "get_report"
|
||||||
|
json_add_string "filename" "string"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "preview"
|
||||||
|
json_add_string "type" "string"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "generate"
|
||||||
|
json_add_string "type" "string"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "send"
|
||||||
|
json_add_string "type" "string"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "schedule"
|
||||||
|
json_add_string "type" "string"
|
||||||
|
json_add_string "frequency" "string"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "delete_report"
|
||||||
|
json_add_string "filename" "string"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "test_email"
|
||||||
|
json_close_object
|
||||||
|
json_add_object "get_config"
|
||||||
|
json_close_object
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: status - Get generator status
|
||||||
|
method_status() {
|
||||||
|
config_load "$CONFIG_NAME"
|
||||||
|
|
||||||
|
local enabled output_dir theme
|
||||||
|
local recipient smtp_server
|
||||||
|
local dev_schedule services_schedule
|
||||||
|
|
||||||
|
config_get enabled global enabled '0'
|
||||||
|
config_get output_dir global output_dir '/www/reports'
|
||||||
|
config_get theme global theme 'dark'
|
||||||
|
|
||||||
|
config_get recipient email recipient ''
|
||||||
|
config_get smtp_server email smtp_server ''
|
||||||
|
|
||||||
|
# Check schedules from cron
|
||||||
|
dev_schedule="off"
|
||||||
|
services_schedule="off"
|
||||||
|
if [ -f /etc/cron.d/secubox-reporter ]; then
|
||||||
|
grep -q "generate dev" /etc/cron.d/secubox-reporter && dev_schedule="daily"
|
||||||
|
grep -q "generate services" /etc/cron.d/secubox-reporter && services_schedule="weekly"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Count reports
|
||||||
|
local report_count=0
|
||||||
|
[ -d "$output_dir" ] && report_count=$(ls -1 "$output_dir"/*.html 2>/dev/null | wc -l)
|
||||||
|
|
||||||
|
# Check for most recent reports
|
||||||
|
local last_dev=""
|
||||||
|
local last_services=""
|
||||||
|
local last_dev_time=""
|
||||||
|
local last_services_time=""
|
||||||
|
|
||||||
|
if [ -f "$output_dir/dev-status.html" ]; then
|
||||||
|
last_dev="dev-status.html"
|
||||||
|
last_dev_time=$(stat -c %Y "$output_dir/dev-status.html" 2>/dev/null || echo "0")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$output_dir/services-status.html" ]; then
|
||||||
|
last_services="services-status.html"
|
||||||
|
last_services_time=$(stat -c %Y "$output_dir/services-status.html" 2>/dev/null || echo "0")
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_boolean "enabled" "$enabled"
|
||||||
|
json_add_string "theme" "$theme"
|
||||||
|
json_add_string "output_dir" "$output_dir"
|
||||||
|
json_add_int "report_count" "$report_count"
|
||||||
|
|
||||||
|
json_add_object "schedules"
|
||||||
|
json_add_string "dev" "$dev_schedule"
|
||||||
|
json_add_string "services" "$services_schedule"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
json_add_object "email"
|
||||||
|
json_add_string "recipient" "$recipient"
|
||||||
|
json_add_string "smtp_server" "$smtp_server"
|
||||||
|
json_add_boolean "configured" "$([ -n "$smtp_server" ] && echo 1 || echo 0)"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
json_add_object "last_reports"
|
||||||
|
json_add_string "dev" "$last_dev"
|
||||||
|
json_add_int "dev_time" "${last_dev_time:-0}"
|
||||||
|
json_add_string "services" "$last_services"
|
||||||
|
json_add_int "services_time" "${last_services_time:-0}"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: list_reports - List all generated reports
|
||||||
|
method_list_reports() {
|
||||||
|
config_load "$CONFIG_NAME"
|
||||||
|
local output_dir
|
||||||
|
config_get output_dir global output_dir '/www/reports'
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_array "reports"
|
||||||
|
|
||||||
|
if [ -d "$output_dir" ]; then
|
||||||
|
for f in "$output_dir"/*.html; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
local filename=$(basename "$f")
|
||||||
|
local size=$(stat -c %s "$f" 2>/dev/null || echo "0")
|
||||||
|
local mtime=$(stat -c %Y "$f" 2>/dev/null || echo "0")
|
||||||
|
local type="unknown"
|
||||||
|
|
||||||
|
case "$filename" in
|
||||||
|
dev-*) type="dev" ;;
|
||||||
|
services-*) type="services" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "filename" "$filename"
|
||||||
|
json_add_string "type" "$type"
|
||||||
|
json_add_int "size" "$size"
|
||||||
|
json_add_int "mtime" "$mtime"
|
||||||
|
json_add_string "url" "/reports/$filename"
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: get_report - Get report content
|
||||||
|
method_get_report() {
|
||||||
|
local filename="$1"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ -z "$filename" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Filename required"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sanitize filename (prevent path traversal)
|
||||||
|
filename=$(basename "$filename")
|
||||||
|
|
||||||
|
config_load "$CONFIG_NAME"
|
||||||
|
local output_dir
|
||||||
|
config_get output_dir global output_dir '/www/reports'
|
||||||
|
|
||||||
|
local filepath="$output_dir/$filename"
|
||||||
|
|
||||||
|
if [ ! -f "$filepath" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Report not found"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get file info
|
||||||
|
local size=$(stat -c %s "$filepath" 2>/dev/null || echo "0")
|
||||||
|
local mtime=$(stat -c %Y "$filepath" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "filename" "$filename"
|
||||||
|
json_add_int "size" "$size"
|
||||||
|
json_add_int "mtime" "$mtime"
|
||||||
|
json_add_string "url" "/reports/$filename"
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: preview - Preview report (generate to stdout)
|
||||||
|
method_preview() {
|
||||||
|
local type="$1"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ -z "$type" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Report type required (dev|services)"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$type" in
|
||||||
|
dev|services|all)
|
||||||
|
local preview=$("$REPORTCTL" preview "$type" 2>&1)
|
||||||
|
local result=$?
|
||||||
|
|
||||||
|
if [ $result -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "type" "$type"
|
||||||
|
# Store preview in temp file and return path (content too large for jshn)
|
||||||
|
local tmpfile="/tmp/reporter-preview-$$.html"
|
||||||
|
echo "$preview" > "$tmpfile"
|
||||||
|
json_add_string "preview_file" "$tmpfile"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Preview failed: $preview"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Invalid type. Use: dev, services, or all"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: generate - Generate a report
|
||||||
|
method_generate() {
|
||||||
|
local type="$1"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ -z "$type" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Report type required (dev|services|all)"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$type" in
|
||||||
|
dev|services|all)
|
||||||
|
local output=$("$REPORTCTL" generate "$type" 2>&1)
|
||||||
|
local result=$?
|
||||||
|
|
||||||
|
if [ $result -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "type" "$type"
|
||||||
|
json_add_string "message" "Report generated successfully"
|
||||||
|
|
||||||
|
# Return generated file info
|
||||||
|
config_load "$CONFIG_NAME"
|
||||||
|
local output_dir
|
||||||
|
config_get output_dir global output_dir '/www/reports'
|
||||||
|
|
||||||
|
if [ "$type" = "dev" ] || [ "$type" = "all" ]; then
|
||||||
|
json_add_string "dev_url" "/reports/dev-status.html"
|
||||||
|
fi
|
||||||
|
if [ "$type" = "services" ] || [ "$type" = "all" ]; then
|
||||||
|
json_add_string "services_url" "/reports/services-status.html"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Generation failed: $output"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Invalid type. Use: dev, services, or all"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: send - Generate and send report via email
|
||||||
|
method_send() {
|
||||||
|
local type="$1"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ -z "$type" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Report type required (dev|services|all)"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$type" in
|
||||||
|
dev|services|all)
|
||||||
|
local output=$("$REPORTCTL" send "$type" 2>&1)
|
||||||
|
local result=$?
|
||||||
|
|
||||||
|
if [ $result -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "type" "$type"
|
||||||
|
json_add_string "message" "Report generated and sent"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Send failed: $output"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Invalid type. Use: dev, services, or all"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: schedule - Set report schedule
|
||||||
|
method_schedule() {
|
||||||
|
local type="$1"
|
||||||
|
local frequency="$2"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ -z "$type" ] || [ -z "$frequency" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Type and frequency required"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$type" in
|
||||||
|
dev|services)
|
||||||
|
local output=$("$REPORTCTL" schedule "$type" "$frequency" 2>&1)
|
||||||
|
local result=$?
|
||||||
|
|
||||||
|
if [ $result -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "type" "$type"
|
||||||
|
json_add_string "frequency" "$frequency"
|
||||||
|
json_add_string "message" "Schedule updated"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Schedule failed: $output"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Invalid type. Use: dev or services"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: delete_report - Delete a report file
|
||||||
|
method_delete_report() {
|
||||||
|
local filename="$1"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ -z "$filename" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Filename required"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sanitize filename
|
||||||
|
filename=$(basename "$filename")
|
||||||
|
|
||||||
|
config_load "$CONFIG_NAME"
|
||||||
|
local output_dir
|
||||||
|
config_get output_dir global output_dir '/www/reports'
|
||||||
|
|
||||||
|
local filepath="$output_dir/$filename"
|
||||||
|
|
||||||
|
if [ ! -f "$filepath" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Report not found"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$filepath"
|
||||||
|
|
||||||
|
if [ ! -f "$filepath" ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Report deleted"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Failed to delete report"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: test_email - Test email configuration
|
||||||
|
method_test_email() {
|
||||||
|
# Source the mailer library
|
||||||
|
. /usr/share/secubox-reporter/lib/mailer.sh
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
local output=$(test_email 2>&1)
|
||||||
|
local result=$?
|
||||||
|
|
||||||
|
if [ $result -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "$output"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$output"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: get_config - Get configuration
|
||||||
|
method_get_config() {
|
||||||
|
config_load "$CONFIG_NAME"
|
||||||
|
|
||||||
|
local enabled output_dir theme history_file
|
||||||
|
local recipient smtp_server smtp_port smtp_user smtp_tls
|
||||||
|
|
||||||
|
config_get enabled global enabled '0'
|
||||||
|
config_get output_dir global output_dir '/www/reports'
|
||||||
|
config_get theme global theme 'dark'
|
||||||
|
|
||||||
|
config_get recipient email recipient ''
|
||||||
|
config_get smtp_server email smtp_server ''
|
||||||
|
config_get smtp_port email smtp_port '587'
|
||||||
|
config_get smtp_user email smtp_user ''
|
||||||
|
config_get smtp_tls email smtp_tls '1'
|
||||||
|
|
||||||
|
config_get history_file sources history_file ''
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
json_add_object "global"
|
||||||
|
json_add_boolean "enabled" "$enabled"
|
||||||
|
json_add_string "output_dir" "$output_dir"
|
||||||
|
json_add_string "theme" "$theme"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
json_add_object "email"
|
||||||
|
json_add_string "recipient" "$recipient"
|
||||||
|
json_add_string "smtp_server" "$smtp_server"
|
||||||
|
json_add_int "smtp_port" "$smtp_port"
|
||||||
|
json_add_string "smtp_user" "$smtp_user"
|
||||||
|
json_add_boolean "smtp_tls" "$smtp_tls"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
json_add_object "sources"
|
||||||
|
json_add_string "history_file" "$history_file"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main dispatcher
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
method_list
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
status)
|
||||||
|
method_status
|
||||||
|
;;
|
||||||
|
list_reports)
|
||||||
|
method_list_reports
|
||||||
|
;;
|
||||||
|
get_report)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var filename filename
|
||||||
|
method_get_report "$filename"
|
||||||
|
;;
|
||||||
|
preview)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var type type
|
||||||
|
method_preview "$type"
|
||||||
|
;;
|
||||||
|
generate)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var type type
|
||||||
|
method_generate "$type"
|
||||||
|
;;
|
||||||
|
send)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var type type
|
||||||
|
method_send "$type"
|
||||||
|
;;
|
||||||
|
schedule)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var type type
|
||||||
|
json_get_var frequency frequency
|
||||||
|
method_schedule "$type" "$frequency"
|
||||||
|
;;
|
||||||
|
delete_report)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var filename filename
|
||||||
|
method_delete_report "$filename"
|
||||||
|
;;
|
||||||
|
test_email)
|
||||||
|
method_test_email
|
||||||
|
;;
|
||||||
|
get_config)
|
||||||
|
method_get_config
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"error":"Unknown method"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"error":"Unknown command"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"admin/secubox/system/reporter": {
|
||||||
|
"title": "Report Generator",
|
||||||
|
"order": 6,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "reporter/overview"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": ["luci-app-reporter"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"luci-app-reporter": {
|
||||||
|
"description": "Grant access to SecuBox Report Generator",
|
||||||
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.reporter": [
|
||||||
|
"status",
|
||||||
|
"list_reports",
|
||||||
|
"get_report",
|
||||||
|
"preview",
|
||||||
|
"get_config"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uci": ["secubox-reporter"]
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.reporter": [
|
||||||
|
"generate",
|
||||||
|
"send",
|
||||||
|
"schedule",
|
||||||
|
"delete_report",
|
||||||
|
"test_email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uci": ["secubox-reporter"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user