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