feat(ollama,netdiag): KISS UI rewrite and thermal monitoring

Ollama:
- Complete KISS UI rewrite with simplified dashboard
- RPC declarations without expect clauses for reliability
- Service controls, model management, and chat interface

Network Diagnostics:
- Add temperature display with color-coded thresholds
- Add error collection and export functionality
- Add port mode switching (speed/duplex/EEE)
- Add collect_errors, get_port_modes, get_temperature RPC methods
- Add set_port_mode RPC method for port configuration
- Fix ACL permissions for new methods

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-01 09:41:30 +01:00
parent 948b8776d8
commit 9435cc6282
5 changed files with 867 additions and 542 deletions

View File

@ -1,577 +1,359 @@
'use strict';
'require view';
'require ui';
'require dom';
'require poll';
'require rpc';
var callStatus = rpc.declare({
object: 'luci.ollama',
method: 'status',
expect: { }
});
var api = {
status: rpc.declare({ object: 'luci.ollama', method: 'status' }),
models: rpc.declare({ object: 'luci.ollama', method: 'models' }),
health: rpc.declare({ object: 'luci.ollama', method: 'health' }),
start: rpc.declare({ object: 'luci.ollama', method: 'start' }),
stop: rpc.declare({ object: 'luci.ollama', method: 'stop' }),
restart: rpc.declare({ object: 'luci.ollama', method: 'restart' }),
pull: rpc.declare({ object: 'luci.ollama', method: 'model_pull', params: ['name'] }),
remove: rpc.declare({ object: 'luci.ollama', method: 'model_remove', params: ['name'] }),
chat: rpc.declare({ object: 'luci.ollama', method: 'chat', params: ['model', 'message'] })
};
var callModels = rpc.declare({
object: 'luci.ollama',
method: 'models',
expect: { models: [] }
});
var callHealth = rpc.declare({
object: 'luci.ollama',
method: 'health',
expect: { healthy: false }
});
var callStart = rpc.declare({
object: 'luci.ollama',
method: 'start',
expect: { success: false }
});
var callStop = rpc.declare({
object: 'luci.ollama',
method: 'stop',
expect: { success: false }
});
var callRestart = rpc.declare({
object: 'luci.ollama',
method: 'restart',
expect: { success: false }
});
function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
function fmtBytes(b) {
if (!b) return '-';
var u = ['B', 'KB', 'MB', 'GB'];
var i = 0;
while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; }
return b.toFixed(1) + ' ' + u[i];
}
function formatUptime(seconds) {
if (!seconds) return 'N/A';
var days = Math.floor(seconds / 86400);
var hours = Math.floor((seconds % 86400) / 3600);
var mins = Math.floor((seconds % 3600) / 60);
if (days > 0) return days + 'd ' + hours + 'h';
if (hours > 0) return hours + 'h ' + mins + 'm';
return mins + 'm';
function fmtUptime(s) {
if (!s) return '-';
var h = Math.floor(s / 3600);
var m = Math.floor((s % 3600) / 60);
return h > 0 ? h + 'h ' + m + 'm' : m + 'm';
}
return view.extend({
title: _('Ollama Dashboard'),
refreshInterval: 5000,
data: null,
css: `
:root { --ol-bg: #0f172a; --ol-card: #1e293b; --ol-border: #334155; --ol-text: #f1f5f9; --ol-muted: #94a3b8; --ol-accent: #f97316; --ol-success: #22c55e; --ol-danger: #ef4444; }
.ol-wrap { font-family: system-ui, sans-serif; background: var(--ol-bg); color: var(--ol-text); min-height: 100vh; padding: 1rem; }
.ol-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--ol-border); }
.ol-title { font-size: 1.5rem; font-weight: 700; display: flex; align-items: center; gap: 0.5rem; }
.ol-title span { font-size: 1.75rem; }
.ol-badge { padding: 0.25rem 0.75rem; border-radius: 1rem; font-size: 0.75rem; font-weight: 600; }
.ol-badge.on { background: rgba(34,197,94,0.2); color: var(--ol-success); }
.ol-badge.off { background: rgba(239,68,68,0.2); color: var(--ol-danger); }
.ol-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.ol-stat { background: var(--ol-card); border: 1px solid var(--ol-border); border-radius: 0.5rem; padding: 1rem; text-align: center; }
.ol-stat-val { font-size: 1.5rem; font-weight: 700; color: var(--ol-accent); }
.ol-stat-lbl { font-size: 0.7rem; color: var(--ol-muted); text-transform: uppercase; margin-top: 0.25rem; }
.ol-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 1rem; }
.ol-card { background: var(--ol-card); border: 1px solid var(--ol-border); border-radius: 0.5rem; overflow: hidden; }
.ol-card-head { padding: 0.75rem 1rem; background: rgba(0,0,0,0.2); border-bottom: 1px solid var(--ol-border); font-weight: 600; display: flex; justify-content: space-between; align-items: center; }
.ol-card-body { padding: 1rem; }
.ol-btn { padding: 0.5rem 1rem; border: none; border-radius: 0.375rem; font-size: 0.8rem; font-weight: 500; cursor: pointer; transition: opacity 0.2s; }
.ol-btn:hover { opacity: 0.8; }
.ol-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.ol-btn-primary { background: var(--ol-accent); color: #fff; }
.ol-btn-success { background: var(--ol-success); color: #fff; }
.ol-btn-danger { background: var(--ol-danger); color: #fff; }
.ol-btn-sm { padding: 0.25rem 0.5rem; font-size: 0.7rem; }
.ol-btns { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.ol-input { width: 100%; padding: 0.5rem; border: 1px solid var(--ol-border); border-radius: 0.375rem; background: var(--ol-bg); color: var(--ol-text); font-size: 0.875rem; }
.ol-input:focus { outline: none; border-color: var(--ol-accent); }
.ol-row { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--ol-border); font-size: 0.875rem; }
.ol-row:last-child { border-bottom: none; }
.ol-row-lbl { color: var(--ol-muted); }
.ol-model { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem; background: var(--ol-bg); border-radius: 0.375rem; margin-bottom: 0.5rem; }
.ol-model:last-child { margin-bottom: 0; }
.ol-model-name { font-weight: 500; }
.ol-model-size { font-size: 0.75rem; color: var(--ol-muted); }
.ol-empty { text-align: center; padding: 2rem; color: var(--ol-muted); }
.ol-chat-box { height: 200px; overflow-y: auto; background: var(--ol-bg); border: 1px solid var(--ol-border); border-radius: 0.375rem; padding: 0.75rem; margin-bottom: 0.75rem; font-size: 0.875rem; }
.ol-chat-msg { margin-bottom: 0.75rem; }
.ol-chat-msg.user { color: var(--ol-accent); }
.ol-chat-msg.ai { color: var(--ol-text); white-space: pre-wrap; }
.ol-chat-input { display: flex; gap: 0.5rem; }
.ol-chat-input input { flex: 1; }
.ol-select { padding: 0.5rem; border: 1px solid var(--ol-border); border-radius: 0.375rem; background: var(--ol-bg); color: var(--ol-text); font-size: 0.875rem; margin-bottom: 0.75rem; }
.ol-toast { position: fixed; bottom: 1rem; right: 1rem; padding: 0.75rem 1rem; border-radius: 0.375rem; font-size: 0.875rem; z-index: 9999; }
.ol-toast.success { background: var(--ol-success); color: #fff; }
.ol-toast.error { background: var(--ol-danger); color: #fff; }
`,
load: function() {
return Promise.all([
callStatus(),
callModels(),
callHealth()
]).then(function(results) {
var modelsData = Array.isArray(results[1]) ? results[1] : [];
return {
status: results[0] || {},
models: modelsData,
health: results[2] || {}
};
});
api.status().catch(function() { return {}; }),
api.models().catch(function() { return { models: [] }; })
]);
},
render: function(data) {
var self = this;
this.data = data;
var status = data[0] || {};
var models = (data[1] && data[1].models) || [];
var view = E('div', { 'class': 'ol-wrap' }, [
E('style', {}, this.css),
var container = E('div', { 'class': 'ollama-dashboard' }, [
// Header
E('div', { 'class': 'oll-header' }, [
E('div', { 'class': 'oll-logo' }, [
E('div', { 'class': 'oll-logo-icon' }, '🦙'),
E('div', { 'class': 'oll-logo-text' }, 'Ollama')
E('div', { 'class': 'ol-header' }, [
E('div', { 'class': 'ol-title' }, [
E('span', {}, '\uD83E\uDD99'),
'Ollama'
]),
E('div', { 'class': 'oll-header-info' }, [
E('div', {
'class': 'oll-status-badge ' + (data.status.running ? '' : 'offline'),
'id': 'oll-status-badge'
}, [
E('span', { 'class': 'oll-status-dot' }),
data.status.running ? _('Running') : _('Stopped')
])
])
E('div', { 'class': 'ol-badge ' + (status.running ? 'on' : 'off'), 'id': 'ol-status' },
status.running ? 'Running' : 'Stopped')
]),
// Quick Stats
E('div', { 'class': 'oll-quick-stats' }, [
E('div', { 'class': 'oll-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #f97316, #ea580c)' }, [
E('div', { 'class': 'oll-quick-stat-header' }, [
E('span', { 'class': 'oll-quick-stat-icon' }, '🧠'),
E('span', { 'class': 'oll-quick-stat-label' }, _('Models'))
]),
E('div', { 'class': 'oll-quick-stat-value', 'id': 'models-count' },
(data.models || []).length.toString()
),
E('div', { 'class': 'oll-quick-stat-sub' }, _('Downloaded'))
]),
// Stats
E('div', { 'class': 'ol-stats', 'id': 'ol-stats' }, this.renderStats(status, models)),
E('div', { 'class': 'oll-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #10b981, #059669)' }, [
E('div', { 'class': 'oll-quick-stat-header' }, [
E('span', { 'class': 'oll-quick-stat-icon' }, '⏱️'),
E('span', { 'class': 'oll-quick-stat-label' }, _('Uptime'))
]),
E('div', { 'class': 'oll-quick-stat-value', 'id': 'uptime' },
data.status.running ? formatUptime(data.status.uptime) : '--'
),
E('div', { 'class': 'oll-quick-stat-sub' }, _('Running'))
]),
E('div', { 'class': 'oll-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #06b6d4, #0ea5e9)' }, [
E('div', { 'class': 'oll-quick-stat-header' }, [
E('span', { 'class': 'oll-quick-stat-icon' }, '🔌'),
E('span', { 'class': 'oll-quick-stat-label' }, _('API Port'))
]),
E('div', { 'class': 'oll-quick-stat-value' }, data.status.api_port || '11434'),
E('div', { 'class': 'oll-quick-stat-sub' }, _('Endpoint'))
]),
E('div', { 'class': 'oll-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #8b5cf6, #7c3aed)' }, [
E('div', { 'class': 'oll-quick-stat-header' }, [
E('span', { 'class': 'oll-quick-stat-icon' }, '🐋'),
E('span', { 'class': 'oll-quick-stat-label' }, _('Runtime'))
]),
E('div', { 'class': 'oll-quick-stat-value' }, data.status.runtime || 'none'),
E('div', { 'class': 'oll-quick-stat-sub' }, _('Container'))
])
]),
// Main Cards Grid
E('div', { 'class': 'oll-cards-grid' }, [
// Service Control Card
E('div', { 'class': 'oll-card' }, [
E('div', { 'class': 'oll-card-header' }, [
E('div', { 'class': 'oll-card-title' }, [
E('span', { 'class': 'oll-card-title-icon' }, '⚙️'),
_('Service Control')
]),
E('div', {
'class': 'oll-card-badge ' + (data.status.running ? 'running' : 'stopped')
}, data.status.running ? _('Active') : _('Inactive'))
]),
E('div', { 'class': 'oll-card-body' }, [
E('div', { 'class': 'oll-service-info' }, [
E('div', { 'class': 'oll-service-row' }, [
E('span', { 'class': 'oll-service-label' }, _('Status')),
E('span', {
'class': 'oll-service-value ' + (data.status.running ? 'running' : 'stopped'),
'id': 'service-status'
}, data.status.running ? _('Running') : _('Stopped'))
// Cards Grid
E('div', { 'class': 'ol-grid' }, [
// Service Control
E('div', { 'class': 'ol-card' }, [
E('div', { 'class': 'ol-card-head' }, 'Service Control'),
E('div', { 'class': 'ol-card-body' }, [
E('div', {}, [
E('div', { 'class': 'ol-row' }, [
E('span', { 'class': 'ol-row-lbl' }, 'Status'),
E('span', { 'id': 'svc-status' }, status.running ? 'Running' : 'Stopped')
]),
E('div', { 'class': 'oll-service-row' }, [
E('span', { 'class': 'oll-service-label' }, _('Memory Limit')),
E('span', { 'class': 'oll-service-value' }, data.status.memory_limit || '2g')
E('div', { 'class': 'ol-row' }, [
E('span', { 'class': 'ol-row-lbl' }, 'Runtime'),
E('span', {}, status.runtime || 'none')
]),
E('div', { 'class': 'oll-service-row' }, [
E('span', { 'class': 'oll-service-label' }, _('Data Path')),
E('span', { 'class': 'oll-service-value' }, data.status.data_path || '/srv/ollama')
E('div', { 'class': 'ol-row' }, [
E('span', { 'class': 'ol-row-lbl' }, 'Memory'),
E('span', {}, status.memory_limit || '2g')
]),
E('div', { 'class': 'ol-row' }, [
E('span', { 'class': 'ol-row-lbl' }, 'Data Path'),
E('span', {}, status.data_path || '/srv/ollama')
])
]),
E('div', { 'class': 'oll-service-controls' }, [
E('button', {
'class': 'oll-btn oll-btn-success' + (data.status.running ? ' disabled' : ''),
'click': function() { self.handleServiceAction('start'); },
'disabled': data.status.running
}, [E('span', {}, '▶'), _('Start')]),
E('button', {
'class': 'oll-btn oll-btn-danger' + (!data.status.running ? ' disabled' : ''),
'click': function() { self.handleServiceAction('stop'); },
'disabled': !data.status.running
}, [E('span', {}, '⏹'), _('Stop')]),
E('button', {
'class': 'oll-btn oll-btn-warning',
'click': function() { self.handleServiceAction('restart'); }
}, [E('span', {}, '🔄'), _('Restart')])
E('div', { 'class': 'ol-btns', 'style': 'margin-top: 1rem;' }, [
E('button', { 'class': 'ol-btn ol-btn-success', 'click': function() { self.svcAction('start'); } }, 'Start'),
E('button', { 'class': 'ol-btn ol-btn-danger', 'click': function() { self.svcAction('stop'); } }, 'Stop'),
E('button', { 'class': 'ol-btn ol-btn-primary', 'click': function() { self.svcAction('restart'); } }, 'Restart')
])
])
]),
// Models Card
E('div', { 'class': 'oll-card' }, [
E('div', { 'class': 'oll-card-header' }, [
E('div', { 'class': 'oll-card-title' }, [
E('span', { 'class': 'oll-card-title-icon' }, '🦙'),
_('Downloaded Models')
]),
E('div', { 'class': 'oll-card-badge' },
(data.models || []).length + ' ' + _('models')
)
// Models
E('div', { 'class': 'ol-card' }, [
E('div', { 'class': 'ol-card-head' }, [
'Models',
E('span', { 'id': 'model-count' }, models.length + ' installed')
]),
E('div', { 'class': 'oll-card-body' }, [
this.renderModelsList(data.models || [])
])
])
]),
// API Info Card
E('div', { 'class': 'oll-card', 'style': 'margin-top: 20px' }, [
E('div', { 'class': 'oll-card-header' }, [
E('div', { 'class': 'oll-card-title' }, [
E('span', { 'class': 'oll-card-title-icon' }, '🔗'),
_('API Endpoints')
E('div', { 'class': 'ol-card-body' }, [
E('div', { 'id': 'ol-models' }, this.renderModels(models)),
E('div', { 'style': 'margin-top: 1rem; display: flex; gap: 0.5rem;' }, [
E('input', { 'type': 'text', 'class': 'ol-input', 'id': 'pull-model', 'placeholder': 'Model name (e.g. tinyllama, llama2)' }),
E('button', { 'class': 'ol-btn ol-btn-primary', 'click': function() { self.pullModel(); } }, 'Pull')
])
])
]),
E('div', { 'class': 'oll-card-body' }, [
E('div', { 'class': 'oll-api-info' }, [
E('div', { 'class': 'oll-api-endpoint' }, [
E('code', {}, 'http://' + window.location.hostname + ':' + (data.status.api_port || '11434') + '/api/chat'),
E('span', { 'class': 'oll-api-method' }, 'POST'),
E('span', { 'class': 'oll-api-desc' }, _('Chat completion'))
]),
E('div', { 'class': 'oll-api-endpoint' }, [
E('code', {}, 'http://' + window.location.hostname + ':' + (data.status.api_port || '11434') + '/api/generate'),
E('span', { 'class': 'oll-api-method' }, 'POST'),
E('span', { 'class': 'oll-api-desc' }, _('Text generation'))
]),
E('div', { 'class': 'oll-api-endpoint' }, [
E('code', {}, 'http://' + window.location.hostname + ':' + (data.status.api_port || '11434') + '/api/tags'),
E('span', { 'class': 'oll-api-method get' }, 'GET'),
E('span', { 'class': 'oll-api-desc' }, _('List models'))
// Chat
E('div', { 'class': 'ol-card' }, [
E('div', { 'class': 'ol-card-head' }, 'Chat'),
E('div', { 'class': 'ol-card-body' }, [
E('select', { 'class': 'ol-select', 'id': 'chat-model', 'style': 'width: 100%;' },
models.length === 0
? [E('option', {}, '-- No models --')]
: models.map(function(m) { return E('option', { 'value': m.name }, m.name); })
),
E('div', { 'class': 'ol-chat-box', 'id': 'chat-box' }),
E('div', { 'class': 'ol-chat-input' }, [
E('input', { 'type': 'text', 'class': 'ol-input', 'id': 'chat-input', 'placeholder': 'Type a message...',
'keypress': function(e) { if (e.key === 'Enter') self.sendChat(); }
}),
E('button', { 'class': 'ol-btn ol-btn-primary', 'id': 'chat-send', 'click': function() { self.sendChat(); } }, 'Send')
])
])
])
])
]);
var style = E('style', {}, this.getCSS());
container.insertBefore(style, container.firstChild);
return container;
poll.add(L.bind(this.refresh, this), 15);
return view;
},
renderModelsList: function(models) {
if (!models || models.length === 0) {
return E('div', { 'class': 'oll-empty' }, [
E('div', { 'class': 'oll-empty-icon' }, '📦'),
E('div', { 'class': 'oll-empty-text' }, _('No models downloaded')),
E('div', { 'class': 'oll-empty-hint' }, [
_('Download a model with: '),
E('code', {}, 'ollamactl pull tinyllama')
])
]);
}
return E('div', { 'class': 'oll-models-list' },
models.map(function(model) {
return E('div', { 'class': 'oll-model-item' }, [
E('div', { 'class': 'oll-model-icon' }, '🦙'),
E('div', { 'class': 'oll-model-info' }, [
E('div', { 'class': 'oll-model-name' }, model.name),
E('div', { 'class': 'oll-model-meta' }, [
model.size > 0 ? E('span', { 'class': 'oll-model-size' }, formatBytes(model.size)) : null
].filter(Boolean))
])
]);
})
);
renderStats: function(status, models) {
return [
E('div', { 'class': 'ol-stat' }, [
E('div', { 'class': 'ol-stat-val' }, models.length.toString()),
E('div', { 'class': 'ol-stat-lbl' }, 'Models')
]),
E('div', { 'class': 'ol-stat' }, [
E('div', { 'class': 'ol-stat-val' }, status.running ? fmtUptime(status.uptime) : '-'),
E('div', { 'class': 'ol-stat-lbl' }, 'Uptime')
]),
E('div', { 'class': 'ol-stat' }, [
E('div', { 'class': 'ol-stat-val' }, (status.api_port || 11434).toString()),
E('div', { 'class': 'ol-stat-lbl' }, 'API Port')
]),
E('div', { 'class': 'ol-stat' }, [
E('div', { 'class': 'ol-stat-val' }, status.runtime || '-'),
E('div', { 'class': 'ol-stat-lbl' }, 'Runtime')
])
];
},
handleServiceAction: function(action) {
renderModels: function(models) {
var self = this;
ui.showModal(_('Service Control'), [
E('p', {}, _('Processing...')),
E('div', { 'class': 'spinning' })
]);
var actionFn;
switch(action) {
case 'start': actionFn = callStart(); break;
case 'stop': actionFn = callStop(); break;
case 'restart': actionFn = callRestart(); break;
if (!models || models.length === 0) {
return E('div', { 'class': 'ol-empty' }, 'No models installed');
}
return E('div', {}, models.map(function(m) {
return E('div', { 'class': 'ol-model' }, [
E('div', {}, [
E('div', { 'class': 'ol-model-name' }, m.name),
E('div', { 'class': 'ol-model-size' }, fmtBytes(m.size))
]),
E('button', { 'class': 'ol-btn ol-btn-danger ol-btn-sm', 'click': function() { self.removeModel(m.name); } }, 'Remove')
]);
}));
},
actionFn.then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Service ' + action + ' successful')), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', result.error || _('Operation failed')), 'error');
refresh: function() {
var self = this;
return Promise.all([
api.status().catch(function() { return {}; }),
api.models().catch(function() { return { models: [] }; })
]).then(function(data) {
var status = data[0] || {};
var models = (data[1] && data[1].models) || [];
var badge = document.getElementById('ol-status');
if (badge) {
badge.className = 'ol-badge ' + (status.running ? 'on' : 'off');
badge.textContent = status.running ? 'Running' : 'Stopped';
}
var statsEl = document.getElementById('ol-stats');
if (statsEl) dom.content(statsEl, self.renderStats(status, models));
var modelsEl = document.getElementById('ol-models');
if (modelsEl) dom.content(modelsEl, self.renderModels(models));
var countEl = document.getElementById('model-count');
if (countEl) countEl.textContent = models.length + ' installed';
var svcEl = document.getElementById('svc-status');
if (svcEl) svcEl.textContent = status.running ? 'Running' : 'Stopped';
// Update chat model select
var sel = document.getElementById('chat-model');
if (sel && models.length > 0) {
var current = sel.value;
sel.innerHTML = '';
models.forEach(function(m) {
var opt = document.createElement('option');
opt.value = m.name;
opt.textContent = m.name;
if (m.name === current) opt.selected = true;
sel.appendChild(opt);
});
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', err.message), 'error');
});
},
getCSS: function() {
return `
.ollama-dashboard {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: #030712;
color: #f8fafc;
min-height: 100vh;
padding: 16px;
svcAction: function(action) {
var self = this;
var fn = action === 'start' ? api.start : action === 'stop' ? api.stop : api.restart;
fn().then(function(r) {
self.toast(r && r.success ? action + ' OK' : 'Failed: ' + ((r && r.error) || 'Unknown'), r && r.success);
if (r && r.success) setTimeout(function() { self.refresh(); }, 2000);
}).catch(function(e) { self.toast('Error: ' + e.message, false); });
},
pullModel: function() {
var self = this;
var input = document.getElementById('pull-model');
var name = input ? input.value.trim() : '';
if (!name) { self.toast('Enter model name', false); return; }
self.toast('Pulling ' + name + '...', true);
api.pull(name).then(function(r) {
self.toast(r && r.success ? 'Pulled ' + name : 'Failed: ' + ((r && r.error) || 'Unknown'), r && r.success);
if (r && r.success) {
input.value = '';
self.refresh();
}
.oll-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0 20px;
border-bottom: 1px solid #334155;
margin-bottom: 20px;
}).catch(function(e) { self.toast('Error: ' + e.message, false); });
},
removeModel: function(name) {
var self = this;
if (!confirm('Remove model "' + name + '"?')) return;
api.remove(name).then(function(r) {
self.toast(r && r.success ? 'Removed ' + name : 'Failed: ' + ((r && r.error) || 'Unknown'), r && r.success);
if (r && r.success) self.refresh();
}).catch(function(e) { self.toast('Error: ' + e.message, false); });
},
sendChat: function() {
var self = this;
var modelEl = document.getElementById('chat-model');
var inputEl = document.getElementById('chat-input');
var boxEl = document.getElementById('chat-box');
var btnEl = document.getElementById('chat-send');
var model = modelEl ? modelEl.value : '';
var msg = inputEl ? inputEl.value.trim() : '';
if (!model || model === '-- No models --') { self.toast('Select a model', false); return; }
if (!msg) return;
// Add user message
var userDiv = document.createElement('div');
userDiv.className = 'ol-chat-msg user';
userDiv.textContent = '> ' + msg;
boxEl.appendChild(userDiv);
inputEl.value = '';
inputEl.disabled = true;
btnEl.disabled = true;
boxEl.scrollTop = boxEl.scrollHeight;
api.chat(model, msg).then(function(r) {
var aiDiv = document.createElement('div');
aiDiv.className = 'ol-chat-msg ai';
if (r && r.response) {
aiDiv.textContent = r.response;
} else if (r && r.error) {
aiDiv.textContent = 'Error: ' + r.error;
aiDiv.style.color = '#ef4444';
} else {
aiDiv.textContent = 'No response';
}
.oll-logo {
display: flex;
align-items: center;
gap: 14px;
}
.oll-logo-icon {
width: 46px;
height: 46px;
background: linear-gradient(135deg, #f97316, #ea580c);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.oll-logo-text {
font-size: 24px;
font-weight: 700;
background: linear-gradient(135deg, #f97316, #ea580c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.oll-status-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 24px;
background: rgba(16, 185, 129, 0.15);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
font-weight: 600;
}
.oll-status-badge.offline {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border-color: rgba(239, 68, 68, 0.3);
}
.oll-status-dot {
width: 10px;
height: 10px;
background: currentColor;
border-radius: 50%;
}
.oll-quick-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 14px;
margin-bottom: 24px;
}
.oll-quick-stat {
background: #0f172a;
border: 1px solid #334155;
border-radius: 12px;
padding: 20px;
position: relative;
overflow: hidden;
}
.oll-quick-stat::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--stat-gradient);
}
.oll-quick-stat-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.oll-quick-stat-icon { font-size: 22px; }
.oll-quick-stat-label {
font-size: 11px;
text-transform: uppercase;
color: #64748b;
}
.oll-quick-stat-value {
font-size: 32px;
font-weight: 700;
background: var(--stat-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.oll-quick-stat-sub {
font-size: 11px;
color: #64748b;
margin-top: 6px;
}
.oll-cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
}
.oll-card {
background: #0f172a;
border: 1px solid #334155;
border-radius: 12px;
overflow: hidden;
}
.oll-card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #334155;
background: rgba(0, 0, 0, 0.3);
}
.oll-card-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 15px;
font-weight: 600;
}
.oll-card-title-icon { font-size: 20px; }
.oll-card-badge {
font-size: 12px;
padding: 5px 12px;
border-radius: 16px;
background: linear-gradient(135deg, #f97316, #ea580c);
color: white;
}
.oll-card-badge.running { background: linear-gradient(135deg, #10b981, #059669); }
.oll-card-badge.stopped { background: rgba(100, 116, 139, 0.3); color: #94a3b8; }
.oll-card-body { padding: 20px; }
.oll-service-info {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 20px;
}
.oll-service-row {
display: flex;
justify-content: space-between;
padding: 8px 12px;
background: #030712;
border-radius: 8px;
}
.oll-service-label { color: #94a3b8; font-size: 13px; }
.oll-service-value { font-size: 13px; }
.oll-service-value.running { color: #10b981; }
.oll-service-value.stopped { color: #ef4444; }
.oll-service-controls {
display: flex;
gap: 10px;
}
.oll-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 16px;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
}
.oll-btn-success {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
}
.oll-btn-danger {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
}
.oll-btn-warning {
background: linear-gradient(135deg, #f59e0b, #d97706);
color: white;
}
.oll-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.oll-models-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.oll-model-item {
display: flex;
align-items: center;
gap: 14px;
padding: 14px;
background: #1e293b;
border-radius: 10px;
}
.oll-model-icon {
width: 44px;
height: 44px;
background: linear-gradient(135deg, #f97316, #ea580c);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.oll-model-name {
font-weight: 600;
margin-bottom: 4px;
}
.oll-model-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: #94a3b8;
}
.oll-empty {
text-align: center;
padding: 40px 20px;
color: #64748b;
}
.oll-empty-icon { font-size: 48px; margin-bottom: 12px; }
.oll-empty-text { font-size: 16px; margin-bottom: 8px; }
.oll-empty-hint { font-size: 13px; }
.oll-empty-hint code {
background: #1e293b;
padding: 4px 8px;
border-radius: 4px;
}
.oll-api-info {
display: flex;
flex-direction: column;
gap: 10px;
}
.oll-api-endpoint {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #030712;
border-radius: 8px;
}
.oll-api-endpoint code {
font-size: 12px;
color: #f97316;
flex: 1;
}
.oll-api-method {
padding: 4px 8px;
background: #f97316;
color: #030712;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
}
.oll-api-method.get { background: #10b981; }
.oll-api-desc {
font-size: 12px;
color: #94a3b8;
min-width: 120px;
}
`;
}
boxEl.appendChild(aiDiv);
boxEl.scrollTop = boxEl.scrollHeight;
}).catch(function(e) {
var errDiv = document.createElement('div');
errDiv.className = 'ol-chat-msg ai';
errDiv.textContent = 'Error: ' + e.message;
errDiv.style.color = '#ef4444';
boxEl.appendChild(errDiv);
}).finally(function() {
inputEl.disabled = false;
btnEl.disabled = false;
inputEl.focus();
});
},
toast: function(msg, success) {
var t = document.querySelector('.ol-toast');
if (t) t.remove();
t = document.createElement('div');
t.className = 'ol-toast ' + (success ? 'success' : 'error');
t.textContent = msg;
document.body.appendChild(t);
setTimeout(function() { t.remove(); }, 4000);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -83,6 +83,99 @@
color: var(--sb-text);
}
/* Header right section */
.netdiag-header-right {
display: flex;
align-items: center;
gap: 16px;
}
/* Temperature display */
.netdiag-temp-display {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: rgba(255,255,255,0.1);
border-radius: 8px;
font-weight: 600;
}
.netdiag-temp-display .temp-icon {
font-size: 1.1rem;
}
.netdiag-temp-display .temp-value {
font-size: 1.1rem;
font-family: 'JetBrains Mono', monospace;
}
.netdiag-temp-display .temp-unit {
font-size: 0.85rem;
opacity: 0.8;
}
.netdiag-temp-display.temp-normal {
background: rgba(87, 204, 153, 0.2);
color: var(--sb-success);
}
.netdiag-temp-display.temp-warm {
background: rgba(255, 202, 58, 0.2);
color: var(--sb-warning);
}
.netdiag-temp-display.temp-hot {
background: rgba(255, 89, 94, 0.2);
color: var(--sb-critical);
animation: pulse-temp 1.5s infinite;
}
@keyframes pulse-temp {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* Port mode controls */
.netdiag-port-mode {
background: var(--sb-bg-dark);
border-radius: 8px;
padding: 16px;
}
.netdiag-mode-controls {
display: flex;
gap: 16px;
align-items: flex-end;
flex-wrap: wrap;
}
.netdiag-mode-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.netdiag-mode-group label {
font-size: 0.8rem;
color: var(--sb-text-muted);
}
.netdiag-select {
padding: 8px 12px;
background: var(--sb-bg-card);
border: 1px solid var(--sb-border);
border-radius: 6px;
color: var(--sb-text);
font-size: 0.9rem;
min-width: 180px;
}
.netdiag-select:focus {
outline: none;
border-color: var(--sb-primary);
}
/* Section containers */
.netdiag-section {
background: var(--sb-bg-card);

View File

@ -38,6 +38,32 @@ var callClearCounters = rpc.declare({
expect: {}
});
var callCollectErrors = rpc.declare({
object: 'luci.secubox-netdiag',
method: 'collect_errors',
expect: {}
});
var callGetPortModes = rpc.declare({
object: 'luci.secubox-netdiag',
method: 'get_port_modes',
params: ['interface'],
expect: {}
});
var callSetPortMode = rpc.declare({
object: 'luci.secubox-netdiag',
method: 'set_port_mode',
params: ['interface', 'speed', 'duplex', 'eee', 'autoneg'],
expect: {}
});
var callGetTemperature = rpc.declare({
object: 'luci.secubox-netdiag',
method: 'get_temperature',
expect: {}
});
return view.extend({
refreshInterval: 5000,
pollHandle: null,
@ -79,27 +105,141 @@ return view.extend({
E('span', { 'class': 'netdiag-title-icon' }, '\uD83D\uDD0C'),
_('Network Diagnostics')
]),
E('div', { 'class': 'netdiag-refresh-control' }, [
E('div', { 'class': 'netdiag-header-right' }, [
E('div', { 'class': 'netdiag-temp-display', 'id': 'temp-display' }, [
E('span', { 'class': 'temp-icon' }, '\uD83C\uDF21\uFE0F'),
E('span', { 'class': 'temp-value', 'id': 'temp-value' }, '--'),
E('span', { 'class': 'temp-unit' }, '\u00B0C')
]),
E('button', {
'class': 'netdiag-refresh-btn',
'click': function() { self.refreshData(); }
}, '\u21BB ' + _('Refresh')),
E('select', {
'class': 'netdiag-refresh-select',
'change': function(ev) {
self.refreshInterval = parseInt(ev.target.value, 10);
self.restartPolling();
}
}, [
E('option', { 'value': '5000', 'selected': true }, _('5 seconds')),
E('option', { 'value': '10000' }, _('10 seconds')),
E('option', { 'value': '30000' }, _('30 seconds')),
E('option', { 'value': '0' }, _('Manual'))
'class': 'netdiag-btn netdiag-btn-secondary',
'click': function() { self.collectAndExportErrors(); },
'title': _('Collect all errors and export')
}, '\uD83D\uDCCB ' + _('Collect Errors')),
E('div', { 'class': 'netdiag-refresh-control' }, [
E('button', {
'class': 'netdiag-refresh-btn',
'click': function() { self.refreshData(); }
}, '\u21BB ' + _('Refresh')),
E('select', {
'class': 'netdiag-refresh-select',
'change': function(ev) {
self.refreshInterval = parseInt(ev.target.value, 10);
self.restartPolling();
}
}, [
E('option', { 'value': '5000', 'selected': true }, _('5 seconds')),
E('option', { 'value': '10000' }, _('10 seconds')),
E('option', { 'value': '30000' }, _('30 seconds')),
E('option', { 'value': '0' }, _('Manual'))
])
])
])
]);
},
updateTemperature: function() {
callGetTemperature().then(function(data) {
var tempEl = document.getElementById('temp-value');
if (!tempEl) return;
var temp = '--';
var zones = data.zones || [];
var hwmon = data.hwmon || [];
// Prefer CPU/SoC temp
for (var i = 0; i < zones.length; i++) {
if (zones[i].temp_c > 0) {
temp = zones[i].temp_c;
break;
}
}
// Fallback to hwmon
if (temp === '--' && hwmon.length > 0) {
for (var j = 0; j < hwmon.length; j++) {
if (hwmon[j].temp_c > 0) {
temp = hwmon[j].temp_c;
break;
}
}
}
tempEl.textContent = temp;
// Color based on temp
var display = document.getElementById('temp-display');
if (display && temp !== '--') {
display.classList.remove('temp-normal', 'temp-warm', 'temp-hot');
if (temp >= 70) display.classList.add('temp-hot');
else if (temp >= 55) display.classList.add('temp-warm');
else display.classList.add('temp-normal');
}
}).catch(function() {});
},
collectAndExportErrors: function() {
var self = this;
ui.showModal(_('Collecting Errors'), [
E('p', {}, _('Gathering error data from all interfaces...')),
E('div', { 'class': 'spinning' })
]);
callCollectErrors().then(function(data) {
ui.hideModal();
var content = 'SecuBox Network Diagnostics - Error Collection\n';
content += '==============================================\n';
content += 'Timestamp: ' + (data.timestamp || new Date().toISOString()) + '\n';
content += 'Hostname: ' + (data.hostname || 'unknown') + '\n';
if (data.temperature) content += 'Temperature: ' + data.temperature + '\u00B0C\n';
content += '\n';
var interfaces = data.interfaces || [];
if (interfaces.length === 0) {
content += 'No errors detected on any interface.\n';
} else {
content += 'INTERFACE ERRORS\n';
content += '----------------\n';
interfaces.forEach(function(iface) {
content += '\n[' + iface.interface + '] Total: ' + iface.total_errors + '\n';
content += ' rx_crc_errors: ' + iface.rx_crc_errors + '\n';
content += ' rx_frame_errors: ' + iface.rx_frame_errors + '\n';
content += ' rx_fifo_errors: ' + iface.rx_fifo_errors + '\n';
content += ' rx_dropped: ' + iface.rx_dropped + '\n';
content += ' tx_dropped: ' + iface.tx_dropped + '\n';
content += ' collisions: ' + iface.collisions + '\n';
});
}
var dmesg = data.dmesg_errors || [];
if (dmesg.length > 0) {
content += '\nKERNEL MESSAGES (errors/warnings)\n';
content += '---------------------------------\n';
dmesg.forEach(function(line) {
content += line + '\n';
});
}
// Download file
var blob = new Blob([content], { type: 'text/plain' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'netdiag-errors-' + Date.now() + '.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
ui.addNotification(null, E('p', _('Error report exported')), 'info');
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Failed to collect errors: ') + err), 'error');
});
},
renderSwitchSection: function(ports, topology) {
var self = this;
var switches = topology.switches || [];
@ -447,6 +587,43 @@ return view.extend({
)
]) : E('div'),
// Port Mode Settings (for temperature control)
E('div', { 'class': 'netdiag-detail-section' }, [
E('div', { 'class': 'netdiag-detail-title' }, [
'\uD83C\uDF21\uFE0F ',
_('Port Mode (Temperature Control)')
]),
E('div', { 'class': 'netdiag-port-mode', 'id': 'port-mode-' + iface }, [
E('p', { 'style': 'color: #94a3b8; font-size: 0.85rem; margin-bottom: 1rem;' },
_('Reducing port speed or enabling EEE can lower heat generation.')),
E('div', { 'class': 'netdiag-mode-controls' }, [
E('div', { 'class': 'netdiag-mode-group' }, [
E('label', {}, _('Speed/Duplex')),
E('select', { 'id': 'speed-select-' + iface, 'class': 'netdiag-select' }, [
E('option', { 'value': 'auto' }, _('Auto-negotiate')),
E('option', { 'value': '1000-full' }, '1000 Mbps Full'),
E('option', { 'value': '100-full' }, '100 Mbps Full'),
E('option', { 'value': '100-half' }, '100 Mbps Half'),
E('option', { 'value': '10-full' }, '10 Mbps Full'),
E('option', { 'value': '10-half' }, '10 Mbps Half')
])
]),
E('div', { 'class': 'netdiag-mode-group' }, [
E('label', {}, _('Energy Efficient Ethernet (EEE)')),
E('select', { 'id': 'eee-select-' + iface, 'class': 'netdiag-select' }, [
E('option', { 'value': '' }, _('No change')),
E('option', { 'value': 'on' }, _('Enable (saves power/heat)')),
E('option', { 'value': 'off' }, _('Disable'))
])
]),
E('button', {
'class': 'netdiag-btn netdiag-btn-primary',
'click': function() { self.applyPortMode(iface); }
}, _('Apply'))
])
])
]),
// Actions
E('div', { 'class': 'netdiag-actions' }, [
E('button', {
@ -465,6 +642,45 @@ return view.extend({
]);
},
applyPortMode: function(iface) {
var self = this;
var speedSelect = document.getElementById('speed-select-' + iface);
var eeeSelect = document.getElementById('eee-select-' + iface);
if (!speedSelect) return;
var speedVal = speedSelect.value;
var eeeVal = eeeSelect ? eeeSelect.value : '';
var speed = '', duplex = '', autoneg = '';
if (speedVal === 'auto') {
autoneg = 'on';
} else {
var parts = speedVal.split('-');
speed = parts[0];
duplex = parts[1];
autoneg = 'off';
}
ui.showModal(_('Applying Port Mode'), [
E('p', {}, _('Changing port settings for %s...').format(iface)),
E('div', { 'class': 'spinning' })
]);
callSetPortMode(iface, speed, duplex, eeeVal, autoneg).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', result.message || _('Port mode updated')), 'info');
self.refreshData();
} else {
ui.addNotification(null, E('p', _('Failed: ') + (result.error || _('Unknown error'))), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: ') + err), 'error');
});
},
renderDetailItem: function(label, value) {
return E('div', { 'class': 'netdiag-detail-item' }, [
E('span', { 'class': 'netdiag-detail-label' }, label),
@ -560,9 +776,13 @@ return view.extend({
startPolling: function() {
var self = this;
// Initial temperature update
this.updateTemperature();
if (this.refreshInterval > 0) {
this.pollHandle = poll.add(function() {
self.refreshData();
self.updateTemperature();
}, this.refreshInterval / 1000);
}
},

View File

@ -478,10 +478,216 @@ get_topology() {
json_dump
}
# Method: collect_errors
# Collect comprehensive error logs from all interfaces
collect_errors() {
json_init
json_add_string "timestamp" "$(date -Iseconds)"
json_add_string "hostname" "$(cat /proc/sys/kernel/hostname)"
# System temperature if available
local temp=""
for t in /sys/class/thermal/thermal_zone*/temp; do
[ -f "$t" ] && temp=$(cat "$t" 2>/dev/null)
break
done
[ -n "$temp" ] && json_add_int "temperature" "$((temp / 1000))"
# Collect errors per interface
json_add_array "interfaces"
for iface_path in /sys/class/net/*; do
[ ! -d "$iface_path" ] && continue
local iface=$(basename "$iface_path")
case "$iface" in lo|br-*|docker*|veth*|tun*|tap*) continue ;; esac
local rx_crc=$(read_stat "$iface" rx_crc_errors)
local rx_frame=$(read_stat "$iface" rx_frame_errors)
local rx_fifo=$(read_stat "$iface" rx_fifo_errors)
local rx_dropped=$(read_stat "$iface" rx_dropped)
local tx_dropped=$(read_stat "$iface" tx_dropped)
local collisions=$(read_stat "$iface" collisions)
local total_err=$((rx_crc + rx_frame + rx_fifo + rx_dropped + tx_dropped + collisions))
[ "$total_err" -eq 0 ] && continue
json_add_object ""
json_add_string "interface" "$iface"
json_add_int "rx_crc_errors" "$rx_crc"
json_add_int "rx_frame_errors" "$rx_frame"
json_add_int "rx_fifo_errors" "$rx_fifo"
json_add_int "rx_dropped" "$rx_dropped"
json_add_int "tx_dropped" "$tx_dropped"
json_add_int "collisions" "$collisions"
json_add_int "total_errors" "$total_err"
json_close_object
done
json_close_array
# Recent dmesg errors
json_add_array "dmesg_errors"
dmesg 2>/dev/null | grep -iE "error|fail|timeout|reset|link" | tail -30 | while read -r line; do
json_add_string "" "$line"
done
json_close_array
json_dump
}
# Method: get_port_modes
# Get current and available speed/duplex modes for an interface
get_port_modes() {
local iface="$1"
if [ ! -d "/sys/class/net/${iface}" ]; then
json_init
json_add_boolean "error" 1
json_add_string "message" "Interface not found: $iface"
json_dump
return
fi
json_init
json_add_string "interface" "$iface"
# Current settings
local ethtool_out=$(ethtool "$iface" 2>/dev/null)
if [ -n "$ethtool_out" ]; then
local cur_speed=$(echo "$ethtool_out" | grep -i 'Speed:' | head -1 | awk '{print $2}')
local cur_duplex=$(echo "$ethtool_out" | grep -i 'Duplex:' | head -1 | awk '{print $2}')
local cur_autoneg=$(echo "$ethtool_out" | grep -E '^\s+Auto-negotiation:' | head -1 | awk '{print $2}')
local cur_link=$(echo "$ethtool_out" | grep -i 'Link detected:' | head -1 | awk '{print $3}')
json_add_string "current_speed" "${cur_speed:-unknown}"
json_add_string "current_duplex" "${cur_duplex:-unknown}"
json_add_string "autoneg" "${cur_autoneg:-unknown}"
json_add_string "link" "${cur_link:-unknown}"
# Parse supported modes
json_add_array "supported_speeds"
echo "$ethtool_out" | grep -A50 'Supported link modes:' | grep -oE '[0-9]+base[A-Za-z/]+' | sort -u | while read -r mode; do
json_add_string "" "$mode"
done
json_close_array
fi
# EEE status (Energy Efficient Ethernet) - reduces heat
local eee_out=$(ethtool --show-eee "$iface" 2>/dev/null)
if [ -n "$eee_out" ]; then
local eee_enabled=$(echo "$eee_out" | grep -i "EEE status:" | grep -q "enabled" && echo "true" || echo "false")
local eee_active=$(echo "$eee_out" | grep -i "Link partner" | grep -q "Yes" && echo "true" || echo "false")
json_add_boolean "eee_enabled" "$eee_enabled"
json_add_boolean "eee_active" "$eee_active"
json_add_boolean "eee_supported" "true"
else
json_add_boolean "eee_supported" "false"
fi
# Wake-on-LAN status
local wol_out=$(ethtool "$iface" 2>/dev/null | grep -E '^\s+Wake-on:' | head -1 | awk '{print $2}')
json_add_string "wake_on_lan" "${wol_out:-d}"
json_dump
}
# Method: set_port_mode
# Set interface speed/duplex/EEE for temperature control
set_port_mode() {
local iface="$1"
local speed="$2"
local duplex="$3"
local eee="$4"
local autoneg="$5"
if [ ! -d "/sys/class/net/${iface}" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Interface not found: $iface"
json_dump
return
fi
local result=""
local error=""
# Set speed/duplex
if [ -n "$speed" ] && [ -n "$duplex" ]; then
if [ "$autoneg" = "on" ]; then
result=$(ethtool -s "$iface" autoneg on 2>&1)
else
result=$(ethtool -s "$iface" speed "$speed" duplex "$duplex" autoneg off 2>&1)
fi
[ $? -ne 0 ] && error="$result"
fi
# Set EEE (Energy Efficient Ethernet) - key for temperature
if [ -n "$eee" ]; then
if [ "$eee" = "on" ]; then
result=$(ethtool --set-eee "$iface" eee on 2>&1)
else
result=$(ethtool --set-eee "$iface" eee off 2>&1)
fi
[ $? -ne 0 ] && [ -z "$error" ] && error="EEE: $result"
fi
json_init
if [ -z "$error" ]; then
json_add_boolean "success" 1
json_add_string "message" "Port mode updated for $iface"
# Log the change
logger -t secubox-netdiag "Port mode changed: $iface speed=$speed duplex=$duplex eee=$eee"
else
json_add_boolean "success" 0
json_add_string "error" "$error"
fi
json_dump
}
# Method: get_temperature
# Get system and interface temperatures
get_temperature() {
json_init
# CPU/SoC temperature
json_add_array "zones"
for zone in /sys/class/thermal/thermal_zone*; do
[ ! -d "$zone" ] && continue
local name=$(cat "$zone/type" 2>/dev/null || basename "$zone")
local temp=$(cat "$zone/temp" 2>/dev/null || echo 0)
json_add_object ""
json_add_string "name" "$name"
json_add_int "temp_mc" "$temp"
json_add_int "temp_c" "$((temp / 1000))"
json_close_object
done
json_close_array
# hwmon temperatures
json_add_array "hwmon"
for hwmon in /sys/class/hwmon/hwmon*; do
[ ! -d "$hwmon" ] && continue
local name=$(cat "$hwmon/name" 2>/dev/null || basename "$hwmon")
for temp_file in "$hwmon"/temp*_input; do
[ ! -f "$temp_file" ] && continue
local temp=$(cat "$temp_file" 2>/dev/null || echo 0)
local label_file="${temp_file%_input}_label"
local label=$(cat "$label_file" 2>/dev/null || basename "$temp_file" .input)
json_add_object ""
json_add_string "sensor" "$name"
json_add_string "label" "$label"
json_add_int "temp_mc" "$temp"
json_add_int "temp_c" "$((temp / 1000))"
json_close_object
done
done
json_close_array
json_dump
}
# RPCD list handler
case "$1" in
list)
echo '{"get_switch_status":{},"get_interface_details":{"interface":"string"},"get_error_history":{"interface":"string","minutes":5},"clear_counters":{"interface":"string"},"get_topology":{}}'
echo '{"get_switch_status":{},"get_interface_details":{"interface":"string"},"get_error_history":{"interface":"string","minutes":5},"clear_counters":{"interface":"string"},"get_topology":{},"collect_errors":{},"get_port_modes":{"interface":"string"},"set_port_mode":{"interface":"string","speed":"string","duplex":"string","eee":"string","autoneg":"string"},"get_temperature":{}}'
;;
call)
case "$2" in
@ -507,6 +713,26 @@ case "$1" in
get_topology)
get_topology
;;
collect_errors)
collect_errors
;;
get_port_modes)
read -r input
iface=$(echo "$input" | jsonfilter -e '@.interface' 2>/dev/null)
get_port_modes "$iface"
;;
set_port_mode)
read -r input
iface=$(echo "$input" | jsonfilter -e '@.interface' 2>/dev/null)
speed=$(echo "$input" | jsonfilter -e '@.speed' 2>/dev/null)
duplex=$(echo "$input" | jsonfilter -e '@.duplex' 2>/dev/null)
eee=$(echo "$input" | jsonfilter -e '@.eee' 2>/dev/null)
autoneg=$(echo "$input" | jsonfilter -e '@.autoneg' 2>/dev/null)
set_port_mode "$iface" "$speed" "$duplex" "$eee" "$autoneg"
;;
get_temperature)
get_temperature
;;
esac
;;
esac

View File

@ -3,19 +3,23 @@
"description": "Grant access to SecuBox Network Diagnostics",
"read": {
"ubus": {
"luci.secubox-netdiag": ["get_switch_status", "get_interface_details", "get_error_history", "get_topology"]
"luci.secubox-netdiag": ["get_switch_status", "get_interface_details", "get_error_history", "get_topology", "collect_errors", "get_port_modes", "get_temperature"]
},
"file": {
"/sys/class/net/*/statistics/*": ["read"],
"/sys/class/net/*/carrier": ["read"],
"/sys/class/net/*/operstate": ["read"],
"/sys/class/net/*/address": ["read"],
"/sys/class/net/*/mtu": ["read"]
"/sys/class/net/*/mtu": ["read"],
"/sys/class/thermal/thermal_zone*/temp": ["read"],
"/sys/class/thermal/thermal_zone*/type": ["read"],
"/sys/class/hwmon/hwmon*/temp*": ["read"],
"/sys/class/hwmon/hwmon*/name": ["read"]
}
},
"write": {
"ubus": {
"luci.secubox-netdiag": ["clear_counters"]
"luci.secubox-netdiag": ["clear_counters", "set_port_mode"]
}
}
}