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:
parent
948b8776d8
commit
9435cc6282
@ -1,577 +1,359 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
'require view';
|
'require view';
|
||||||
'require ui';
|
'require dom';
|
||||||
|
'require poll';
|
||||||
'require rpc';
|
'require rpc';
|
||||||
|
|
||||||
var callStatus = rpc.declare({
|
var api = {
|
||||||
object: 'luci.ollama',
|
status: rpc.declare({ object: 'luci.ollama', method: 'status' }),
|
||||||
method: 'status',
|
models: rpc.declare({ object: 'luci.ollama', method: 'models' }),
|
||||||
expect: { }
|
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({
|
function fmtBytes(b) {
|
||||||
object: 'luci.ollama',
|
if (!b) return '-';
|
||||||
method: 'models',
|
var u = ['B', 'KB', 'MB', 'GB'];
|
||||||
expect: { models: [] }
|
var i = 0;
|
||||||
});
|
while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; }
|
||||||
|
return b.toFixed(1) + ' ' + u[i];
|
||||||
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 formatUptime(seconds) {
|
function fmtUptime(s) {
|
||||||
if (!seconds) return 'N/A';
|
if (!s) return '-';
|
||||||
var days = Math.floor(seconds / 86400);
|
var h = Math.floor(s / 3600);
|
||||||
var hours = Math.floor((seconds % 86400) / 3600);
|
var m = Math.floor((s % 3600) / 60);
|
||||||
var mins = Math.floor((seconds % 3600) / 60);
|
return h > 0 ? h + 'h ' + m + 'm' : m + 'm';
|
||||||
if (days > 0) return days + 'd ' + hours + 'h';
|
|
||||||
if (hours > 0) return hours + 'h ' + mins + 'm';
|
|
||||||
return mins + 'm';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return view.extend({
|
return view.extend({
|
||||||
title: _('Ollama Dashboard'),
|
css: `
|
||||||
refreshInterval: 5000,
|
: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; }
|
||||||
data: null,
|
.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() {
|
load: function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
callStatus(),
|
api.status().catch(function() { return {}; }),
|
||||||
callModels(),
|
api.models().catch(function() { return { models: [] }; })
|
||||||
callHealth()
|
]);
|
||||||
]).then(function(results) {
|
|
||||||
var modelsData = Array.isArray(results[1]) ? results[1] : [];
|
|
||||||
return {
|
|
||||||
status: results[0] || {},
|
|
||||||
models: modelsData,
|
|
||||||
health: results[2] || {}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
render: function(data) {
|
||||||
var self = this;
|
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
|
// Header
|
||||||
E('div', { 'class': 'oll-header' }, [
|
E('div', { 'class': 'ol-header' }, [
|
||||||
E('div', { 'class': 'oll-logo' }, [
|
E('div', { 'class': 'ol-title' }, [
|
||||||
E('div', { 'class': 'oll-logo-icon' }, '🦙'),
|
E('span', {}, '\uD83E\uDD99'),
|
||||||
E('div', { 'class': 'oll-logo-text' }, 'Ollama')
|
'Ollama'
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'oll-header-info' }, [
|
E('div', { 'class': 'ol-badge ' + (status.running ? 'on' : 'off'), 'id': 'ol-status' },
|
||||||
E('div', {
|
status.running ? 'Running' : 'Stopped')
|
||||||
'class': 'oll-status-badge ' + (data.status.running ? '' : 'offline'),
|
]),
|
||||||
'id': 'oll-status-badge'
|
|
||||||
}, [
|
// Stats
|
||||||
E('span', { 'class': 'oll-status-dot' }),
|
E('div', { 'class': 'ol-stats', 'id': 'ol-stats' }, this.renderStats(status, models)),
|
||||||
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': 'ol-row' }, [
|
||||||
|
E('span', { 'class': 'ol-row-lbl' }, 'Runtime'),
|
||||||
|
E('span', {}, status.runtime || 'none')
|
||||||
|
]),
|
||||||
|
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': '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')
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// Quick Stats
|
// Models
|
||||||
E('div', { 'class': 'oll-quick-stats' }, [
|
E('div', { 'class': 'ol-card' }, [
|
||||||
E('div', { 'class': 'oll-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #f97316, #ea580c)' }, [
|
E('div', { 'class': 'ol-card-head' }, [
|
||||||
E('div', { 'class': 'oll-quick-stat-header' }, [
|
'Models',
|
||||||
E('span', { 'class': 'oll-quick-stat-icon' }, '🧠'),
|
E('span', { 'id': 'model-count' }, models.length + ' installed')
|
||||||
E('span', { 'class': 'oll-quick-stat-label' }, _('Models'))
|
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'oll-quick-stat-value', 'id': 'models-count' },
|
E('div', { 'class': 'ol-card-body' }, [
|
||||||
(data.models || []).length.toString()
|
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')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// 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': 'oll-quick-stat-sub' }, _('Downloaded'))
|
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...',
|
||||||
E('div', { 'class': 'oll-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #10b981, #059669)' }, [
|
'keypress': function(e) { if (e.key === 'Enter') self.sendChat(); }
|
||||||
E('div', { 'class': 'oll-quick-stat-header' }, [
|
}),
|
||||||
E('span', { 'class': 'oll-quick-stat-icon' }, '⏱️'),
|
E('button', { 'class': 'ol-btn ol-btn-primary', 'id': 'chat-send', 'click': function() { self.sendChat(); } }, 'Send')
|
||||||
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'))
|
|
||||||
]),
|
|
||||||
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': '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': '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')])
|
|
||||||
])
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
|
|
||||||
// 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')
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
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': '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'))
|
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
var style = E('style', {}, this.getCSS());
|
poll.add(L.bind(this.refresh, this), 15);
|
||||||
container.insertBefore(style, container.firstChild);
|
return view;
|
||||||
|
|
||||||
return container;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderModelsList: function(models) {
|
renderStats: function(status, models) {
|
||||||
if (!models || models.length === 0) {
|
return [
|
||||||
return E('div', { 'class': 'oll-empty' }, [
|
E('div', { 'class': 'ol-stat' }, [
|
||||||
E('div', { 'class': 'oll-empty-icon' }, '📦'),
|
E('div', { 'class': 'ol-stat-val' }, models.length.toString()),
|
||||||
E('div', { 'class': 'oll-empty-text' }, _('No models downloaded')),
|
E('div', { 'class': 'ol-stat-lbl' }, 'Models')
|
||||||
E('div', { 'class': 'oll-empty-hint' }, [
|
]),
|
||||||
_('Download a model with: '),
|
E('div', { 'class': 'ol-stat' }, [
|
||||||
E('code', {}, 'ollamactl pull tinyllama')
|
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')
|
||||||
])
|
])
|
||||||
]);
|
];
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleServiceAction: function(action) {
|
renderModels: function(models) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
if (!models || models.length === 0) {
|
||||||
ui.showModal(_('Service Control'), [
|
return E('div', { 'class': 'ol-empty' }, 'No models installed');
|
||||||
E('p', {}, _('Processing...')),
|
}
|
||||||
E('div', { 'class': 'spinning' })
|
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')
|
||||||
]);
|
]);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
var actionFn;
|
refresh: function() {
|
||||||
switch(action) {
|
var self = this;
|
||||||
case 'start': actionFn = callStart(); break;
|
return Promise.all([
|
||||||
case 'stop': actionFn = callStop(); break;
|
api.status().catch(function() { return {}; }),
|
||||||
case 'restart': actionFn = callRestart(); break;
|
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
actionFn.then(function(result) {
|
var statsEl = document.getElementById('ol-stats');
|
||||||
ui.hideModal();
|
if (statsEl) dom.content(statsEl, self.renderStats(status, models));
|
||||||
if (result.success) {
|
|
||||||
ui.addNotification(null, E('p', _('Service ' + action + ' successful')), 'success');
|
var modelsEl = document.getElementById('ol-models');
|
||||||
window.location.reload();
|
if (modelsEl) dom.content(modelsEl, self.renderModels(models));
|
||||||
} else {
|
|
||||||
ui.addNotification(null, E('p', result.error || _('Operation failed')), 'error');
|
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() {
|
svcAction: function(action) {
|
||||||
return `
|
var self = this;
|
||||||
.ollama-dashboard {
|
var fn = action === 'start' ? api.start : action === 'stop' ? api.stop : api.restart;
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
fn().then(function(r) {
|
||||||
background: #030712;
|
self.toast(r && r.success ? action + ' OK' : 'Failed: ' + ((r && r.error) || 'Unknown'), r && r.success);
|
||||||
color: #f8fafc;
|
if (r && r.success) setTimeout(function() { self.refresh(); }, 2000);
|
||||||
min-height: 100vh;
|
}).catch(function(e) { self.toast('Error: ' + e.message, false); });
|
||||||
padding: 16px;
|
},
|
||||||
|
|
||||||
|
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 {
|
}).catch(function(e) { self.toast('Error: ' + e.message, false); });
|
||||||
display: flex;
|
},
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
removeModel: function(name) {
|
||||||
padding: 12px 0 20px;
|
var self = this;
|
||||||
border-bottom: 1px solid #334155;
|
if (!confirm('Remove model "' + name + '"?')) return;
|
||||||
margin-bottom: 20px;
|
api.remove(name).then(function(r) {
|
||||||
}
|
self.toast(r && r.success ? 'Removed ' + name : 'Failed: ' + ((r && r.error) || 'Unknown'), r && r.success);
|
||||||
.oll-logo {
|
if (r && r.success) self.refresh();
|
||||||
display: flex;
|
}).catch(function(e) { self.toast('Error: ' + e.message, false); });
|
||||||
align-items: center;
|
},
|
||||||
gap: 14px;
|
|
||||||
}
|
sendChat: function() {
|
||||||
.oll-logo-icon {
|
var self = this;
|
||||||
width: 46px;
|
var modelEl = document.getElementById('chat-model');
|
||||||
height: 46px;
|
var inputEl = document.getElementById('chat-input');
|
||||||
background: linear-gradient(135deg, #f97316, #ea580c);
|
var boxEl = document.getElementById('chat-box');
|
||||||
border-radius: 12px;
|
var btnEl = document.getElementById('chat-send');
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
var model = modelEl ? modelEl.value : '';
|
||||||
justify-content: center;
|
var msg = inputEl ? inputEl.value.trim() : '';
|
||||||
font-size: 24px;
|
|
||||||
}
|
if (!model || model === '-- No models --') { self.toast('Select a model', false); return; }
|
||||||
.oll-logo-text {
|
if (!msg) return;
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
// Add user message
|
||||||
background: linear-gradient(135deg, #f97316, #ea580c);
|
var userDiv = document.createElement('div');
|
||||||
-webkit-background-clip: text;
|
userDiv.className = 'ol-chat-msg user';
|
||||||
-webkit-text-fill-color: transparent;
|
userDiv.textContent = '> ' + msg;
|
||||||
}
|
boxEl.appendChild(userDiv);
|
||||||
.oll-status-badge {
|
|
||||||
display: flex;
|
inputEl.value = '';
|
||||||
align-items: center;
|
inputEl.disabled = true;
|
||||||
gap: 8px;
|
btnEl.disabled = true;
|
||||||
padding: 8px 16px;
|
boxEl.scrollTop = boxEl.scrollHeight;
|
||||||
border-radius: 24px;
|
|
||||||
background: rgba(16, 185, 129, 0.15);
|
api.chat(model, msg).then(function(r) {
|
||||||
color: #10b981;
|
var aiDiv = document.createElement('div');
|
||||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
aiDiv.className = 'ol-chat-msg ai';
|
||||||
font-weight: 600;
|
if (r && r.response) {
|
||||||
}
|
aiDiv.textContent = r.response;
|
||||||
.oll-status-badge.offline {
|
} else if (r && r.error) {
|
||||||
background: rgba(239, 68, 68, 0.15);
|
aiDiv.textContent = 'Error: ' + r.error;
|
||||||
color: #ef4444;
|
aiDiv.style.color = '#ef4444';
|
||||||
border-color: rgba(239, 68, 68, 0.3);
|
} else {
|
||||||
}
|
aiDiv.textContent = 'No response';
|
||||||
.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
|
||||||
});
|
});
|
||||||
|
|||||||
@ -83,6 +83,99 @@
|
|||||||
color: var(--sb-text);
|
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 */
|
/* Section containers */
|
||||||
.netdiag-section {
|
.netdiag-section {
|
||||||
background: var(--sb-bg-card);
|
background: var(--sb-bg-card);
|
||||||
|
|||||||
@ -38,6 +38,32 @@ var callClearCounters = rpc.declare({
|
|||||||
expect: {}
|
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({
|
return view.extend({
|
||||||
refreshInterval: 5000,
|
refreshInterval: 5000,
|
||||||
pollHandle: null,
|
pollHandle: null,
|
||||||
@ -79,6 +105,17 @@ return view.extend({
|
|||||||
E('span', { 'class': 'netdiag-title-icon' }, '\uD83D\uDD0C'),
|
E('span', { 'class': 'netdiag-title-icon' }, '\uD83D\uDD0C'),
|
||||||
_('Network Diagnostics')
|
_('Network Diagnostics')
|
||||||
]),
|
]),
|
||||||
|
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-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('div', { 'class': 'netdiag-refresh-control' }, [
|
||||||
E('button', {
|
E('button', {
|
||||||
'class': 'netdiag-refresh-btn',
|
'class': 'netdiag-refresh-btn',
|
||||||
@ -97,9 +134,112 @@ return view.extend({
|
|||||||
E('option', { 'value': '0' }, _('Manual'))
|
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) {
|
renderSwitchSection: function(ports, topology) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var switches = topology.switches || [];
|
var switches = topology.switches || [];
|
||||||
@ -447,6 +587,43 @@ return view.extend({
|
|||||||
)
|
)
|
||||||
]) : E('div'),
|
]) : 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
|
// Actions
|
||||||
E('div', { 'class': 'netdiag-actions' }, [
|
E('div', { 'class': 'netdiag-actions' }, [
|
||||||
E('button', {
|
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) {
|
renderDetailItem: function(label, value) {
|
||||||
return E('div', { 'class': 'netdiag-detail-item' }, [
|
return E('div', { 'class': 'netdiag-detail-item' }, [
|
||||||
E('span', { 'class': 'netdiag-detail-label' }, label),
|
E('span', { 'class': 'netdiag-detail-label' }, label),
|
||||||
@ -560,9 +776,13 @@ return view.extend({
|
|||||||
startPolling: function() {
|
startPolling: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
// Initial temperature update
|
||||||
|
this.updateTemperature();
|
||||||
|
|
||||||
if (this.refreshInterval > 0) {
|
if (this.refreshInterval > 0) {
|
||||||
this.pollHandle = poll.add(function() {
|
this.pollHandle = poll.add(function() {
|
||||||
self.refreshData();
|
self.refreshData();
|
||||||
|
self.updateTemperature();
|
||||||
}, this.refreshInterval / 1000);
|
}, this.refreshInterval / 1000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -478,10 +478,216 @@ get_topology() {
|
|||||||
json_dump
|
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
|
# RPCD list handler
|
||||||
case "$1" in
|
case "$1" in
|
||||||
list)
|
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)
|
call)
|
||||||
case "$2" in
|
case "$2" in
|
||||||
@ -507,6 +713,26 @@ case "$1" in
|
|||||||
get_topology)
|
get_topology)
|
||||||
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
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@ -3,19 +3,23 @@
|
|||||||
"description": "Grant access to SecuBox Network Diagnostics",
|
"description": "Grant access to SecuBox Network Diagnostics",
|
||||||
"read": {
|
"read": {
|
||||||
"ubus": {
|
"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": {
|
"file": {
|
||||||
"/sys/class/net/*/statistics/*": ["read"],
|
"/sys/class/net/*/statistics/*": ["read"],
|
||||||
"/sys/class/net/*/carrier": ["read"],
|
"/sys/class/net/*/carrier": ["read"],
|
||||||
"/sys/class/net/*/operstate": ["read"],
|
"/sys/class/net/*/operstate": ["read"],
|
||||||
"/sys/class/net/*/address": ["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": {
|
"write": {
|
||||||
"ubus": {
|
"ubus": {
|
||||||
"luci.secubox-netdiag": ["clear_counters"]
|
"luci.secubox-netdiag": ["clear_counters", "set_port_mode"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user