|
@@ -329,7 +329,9 @@ function navigate(tabName) {
|
|
|
tools: { title: 'Diagnostic & Troubleshooting Tools', sub: 'Network latency analysis, ICMP/MTR telemetry, path tracing, DNS resolution and TCP probing' },
|
|
tools: { title: 'Diagnostic & Troubleshooting Tools', sub: 'Network latency analysis, ICMP/MTR telemetry, path tracing, DNS resolution and TCP probing' },
|
|
|
persistence: { title: 'Debian Configuration', sub: 'Native /etc/network/interfaces.d/ persistence manager' },
|
|
persistence: { title: 'Debian Configuration', sub: 'Native /etc/network/interfaces.d/ persistence manager' },
|
|
|
settings: { title: 'Router Settings & DNS', sub: 'System DNS forwarder, upstream resolvers, static host mappings, and appliance identity' },
|
|
settings: { title: 'Router Settings & DNS', sub: 'System DNS forwarder, upstream resolvers, static host mappings, and appliance identity' },
|
|
|
- users: { title: 'Local User Manager', sub: 'Manage local system administrator credentials and access permissions' }
|
|
|
|
|
|
|
+ users: { title: 'Local User Manager', sub: 'Manage local system administrator credentials and access permissions' },
|
|
|
|
|
+ services: { title: 'RouterOS Management Services (/ip service)', sub: 'Enable or disable Web, SSH, and Telnet management listeners and listening ports' },
|
|
|
|
|
+ logs: { title: 'System Activity Logs (/log)', sub: 'Real-time circular buffer log viewer and topic-based event monitoring' }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const header = titles[tabName] || titles.dashboard;
|
|
const header = titles[tabName] || titles.dashboard;
|
|
@@ -381,6 +383,8 @@ function navigate(tabName) {
|
|
|
if (tabName === 'bwtest') loadBwTestView();
|
|
if (tabName === 'bwtest') loadBwTestView();
|
|
|
if (tabName === 'settings') loadSettingsData();
|
|
if (tabName === 'settings') loadSettingsData();
|
|
|
if (tabName === 'users') loadUsers();
|
|
if (tabName === 'users') loadUsers();
|
|
|
|
|
+ if (tabName === 'services') loadServices();
|
|
|
|
|
+ if (tabName === 'logs') loadLogs();
|
|
|
setTimeout(initTableSorting, 50);
|
|
setTimeout(initTableSorting, 50);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -16431,6 +16435,166 @@ async function submitSelfPassword(event) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ==================== MANAGEMENT SERVICES (/ip service) ====================
|
|
|
|
|
+state.services = [];
|
|
|
|
|
+
|
|
|
|
|
+async function loadServices(manual = false) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/system/services`);
|
|
|
|
|
+ if (!res.ok) throw new Error('Failed to load services');
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ state.services = data || [];
|
|
|
|
|
+ renderServicesTable();
|
|
|
|
|
+ if (manual) showToast('Services Updated', 'Service status synchronized', 'success');
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ if (manual) showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderServicesTable() {
|
|
|
|
|
+ const tbody = document.getElementById('services-table-body');
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (!state.services || state.services.length === 0) {
|
|
|
|
|
+ tbody.innerHTML = '<tr><td colspan="5" class="py-6 text-center text-slate-500 text-xs">No services configured.</td></tr>';
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tbody.innerHTML = state.services.map(s => {
|
|
|
|
|
+ const isOff = s.disabled;
|
|
|
|
|
+ const statusBadge = isOff
|
|
|
|
|
+ ? '<span class="px-2 py-0.5 rounded text-[10px] bg-rose-500/20 text-rose-300 border border-rose-500/30 font-bold uppercase">Disabled</span>'
|
|
|
|
|
+ : '<span class="px-2 py-0.5 rounded text-[10px] bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 font-bold uppercase">Active</span>';
|
|
|
|
|
+
|
|
|
|
|
+ const toggleBtn = isOff
|
|
|
|
|
+ ? `<button onclick="toggleServiceState('${s.name}', false)" class="px-2.5 py-1 rounded-lg bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30 border border-emerald-500/40 text-[11px] font-bold transition">Enable</button>`
|
|
|
|
|
+ : `<button onclick="toggleServiceState('${s.name}', true)" class="px-2.5 py-1 rounded-lg bg-rose-500/20 text-rose-300 hover:bg-rose-500/30 border border-rose-500/40 text-[11px] font-bold transition">Disable</button>`;
|
|
|
|
|
+
|
|
|
|
|
+ return `
|
|
|
|
|
+ <tr class="hover:bg-slate-800/40 transition">
|
|
|
|
|
+ <td class="py-3 px-4 font-bold text-white uppercase">${s.name}</td>
|
|
|
|
|
+ <td class="py-3 px-4 text-cyan-400 font-mono">${s.port}</td>
|
|
|
|
|
+ <td class="py-3 px-4">${statusBadge}</td>
|
|
|
|
|
+ <td class="py-3 px-4 text-slate-400 text-xs font-sans">${s.comment || '-'}</td>
|
|
|
|
|
+ <td class="py-3 px-4 text-right">
|
|
|
|
|
+ ${toggleBtn}
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function toggleServiceState(serviceName, newDisabled) {
|
|
|
|
|
+ if (serviceName === 'www' && newDisabled) {
|
|
|
|
|
+ if (!confirm("WARNING: Disabling the Web service ('www') will turn off this Web UI and REST API. You will only be able to re-enable it via SSH (port 2222) or Telnet (port 8282) using 'ip service enable www'. Continue?")) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/system/services/${serviceName}/state`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ disabled: newDisabled })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!res.ok) {
|
|
|
|
|
+ const errData = await res.json();
|
|
|
|
|
+ throw new Error(errData.message || 'Failed to update service');
|
|
|
|
|
+ }
|
|
|
|
|
+ showToast('Service Updated', `Service ${serviceName} is now ${newDisabled ? 'disabled' : 'enabled'}`, 'success');
|
|
|
|
|
+ await loadServices();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ==================== SYSTEM LOGS (/log) ====================
|
|
|
|
|
+state.logs = [];
|
|
|
|
|
+let logsTimer = null;
|
|
|
|
|
+
|
|
|
|
|
+async function loadLogs(manual = false) {
|
|
|
|
|
+ const limitEl = document.getElementById('logs-limit-select');
|
|
|
|
|
+ const limit = limitEl ? limitEl.value : '100';
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/system/logs?limit=${limit}`);
|
|
|
|
|
+ if (!res.ok) throw new Error('Failed to load logs');
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ state.logs = data.logs || [];
|
|
|
|
|
+ renderLogsTable();
|
|
|
|
|
+ if (manual) showToast('Logs Refreshed', `Fetched ${state.logs.length} log records`, 'success');
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ if (manual) showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Setup auto-refresh if enabled and on logs tab
|
|
|
|
|
+ if (!logsTimer) {
|
|
|
|
|
+ logsTimer = setInterval(() => {
|
|
|
|
|
+ if (state.activeTab === 'logs') {
|
|
|
|
|
+ const auto = document.getElementById('logs-auto-refresh');
|
|
|
|
|
+ if (auto && auto.checked) {
|
|
|
|
|
+ loadLogs(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 3000);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderLogsTable() {
|
|
|
|
|
+ const tbody = document.getElementById('logs-table-body');
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+
|
|
|
|
|
+ const search = (document.getElementById('logs-search-input')?.value || '').toLowerCase();
|
|
|
|
|
+ const topicFilter = (document.getElementById('logs-topic-filter')?.value || '').toLowerCase();
|
|
|
|
|
+
|
|
|
|
|
+ const filtered = state.logs.filter(entry => {
|
|
|
|
|
+ if (topicFilter && !entry.topics.toLowerCase().includes(topicFilter)) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (search && !entry.message.toLowerCase().includes(search) && !entry.topics.toLowerCase().includes(search)) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (filtered.length === 0) {
|
|
|
|
|
+ tbody.innerHTML = '<tr><td colspan="4" class="py-8 text-center text-slate-500 text-xs">No matching log entries found.</td></tr>';
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tbody.innerHTML = filtered.map(l => {
|
|
|
|
|
+ let topicColor = 'bg-slate-800 text-slate-300 border-slate-700';
|
|
|
|
|
+ const topLower = l.topics.toLowerCase();
|
|
|
|
|
+ if (topLower.includes('auth')) {
|
|
|
|
|
+ topicColor = 'bg-amber-500/20 text-amber-300 border-amber-500/30';
|
|
|
|
|
+ } else if (topLower.includes('dhcp')) {
|
|
|
|
|
+ topicColor = 'bg-cyan-500/20 text-cyan-300 border-cyan-500/30';
|
|
|
|
|
+ } else if (topLower.includes('route')) {
|
|
|
|
|
+ topicColor = 'bg-blue-500/20 text-blue-300 border-blue-500/30';
|
|
|
|
|
+ } else if (topLower.includes('firewall')) {
|
|
|
|
|
+ topicColor = 'bg-rose-500/20 text-rose-300 border-rose-500/30';
|
|
|
|
|
+ } else if (topLower.includes('error')) {
|
|
|
|
|
+ topicColor = 'bg-red-500/20 text-red-300 border-red-500/30';
|
|
|
|
|
+ } else if (topLower.includes('system')) {
|
|
|
|
|
+ topicColor = 'bg-emerald-500/20 text-emerald-300 border-emerald-500/30';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const d = new Date(l.time);
|
|
|
|
|
+ const timeStr = d.toLocaleDateString('en-US', { month: 'short', day: '2-digit' }) + ' ' + d.toLocaleTimeString('en-US', { hour12: false });
|
|
|
|
|
+
|
|
|
|
|
+ return `
|
|
|
|
|
+ <tr class="hover:bg-slate-800/40 transition">
|
|
|
|
|
+ <td class="py-2 px-3 text-slate-500 font-mono">${l.id}</td>
|
|
|
|
|
+ <td class="py-2 px-3 text-slate-400 font-mono whitespace-nowrap">${timeStr}</td>
|
|
|
|
|
+ <td class="py-2 px-3 whitespace-nowrap">
|
|
|
|
|
+ <span class="px-2 py-0.5 rounded text-[10px] font-bold border ${topicColor}">${l.topics}</span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="py-2 px-3 text-slate-200 font-mono break-all">${escapeHtml(l.message)}</td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|