feat(netdiag): Add SecuBox Network Diagnostics dashboard
New LuCI application for DSA switch port monitoring: - Real-time port status (link, speed, duplex) - Error counters (CRC, frame, FIFO, drops) - Alert thresholds (normal/warning/critical) - Interface detail modal with ethtool output - Kernel message logs (dmesg) - Auto-refresh polling (5s/10s/30s) - Export log functionality - SecuBox dark theme styling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fb9bbffc3c
commit
0d9fe9015e
34
package/secubox/luci-app-secubox-netdiag/Makefile
Normal file
34
package/secubox/luci-app-secubox-netdiag/Makefile
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=luci-app-secubox-netdiag
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
PKG_MAINTAINER:=SecuBox Team <secubox@example.com>
|
||||||
|
PKG_LICENSE:=MIT
|
||||||
|
|
||||||
|
LUCI_TITLE:=SecuBox Network Diagnostics Dashboard
|
||||||
|
LUCI_DESCRIPTION:=Real-time DSA switch port statistics, error monitoring, and network health diagnostics
|
||||||
|
LUCI_DEPENDS:=+luci-base +ethtool
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/install
|
||||||
|
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||||
|
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.secubox-netdiag $(1)/usr/libexec/rpcd/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-secubox-netdiag.json $(1)/usr/share/luci/menu.d/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-secubox-netdiag.json $(1)/usr/share/rpcd/acl.d/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/secubox-netdiag
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/secubox-netdiag/*.js $(1)/www/luci-static/resources/view/secubox-netdiag/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/secubox-netdiag
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/secubox-netdiag/*.css $(1)/www/luci-static/resources/secubox-netdiag/
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||||
143
package/secubox/luci-app-secubox-netdiag/README.md
Normal file
143
package/secubox/luci-app-secubox-netdiag/README.md
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
# SecuBox Network Diagnostics
|
||||||
|
|
||||||
|
Real-time DSA switch port statistics and network error monitoring dashboard for OpenWrt.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Switch Port Status Panel**: Visual representation of DSA switch ports with link state, speed, and duplex indicators
|
||||||
|
- **Error Monitoring Widget**: Real-time error tracking with alert thresholds (normal/warning/critical)
|
||||||
|
- **Interface Details**: Full ethtool output, driver statistics, and kernel message logs
|
||||||
|
- **Auto-refresh**: Configurable polling interval (5s, 10s, 30s, or manual)
|
||||||
|
- **Responsive Design**: Mobile-friendly interface with SecuBox dark theme
|
||||||
|
|
||||||
|
## Supported Hardware
|
||||||
|
|
||||||
|
- MOCHAbin (Marvell Armada 8040) with mvpp2 driver
|
||||||
|
- Any OpenWrt device with DSA switch topology
|
||||||
|
- Standalone Ethernet interfaces (non-DSA)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build with SDK
|
||||||
|
cd secubox-tools/sdk
|
||||||
|
make package/luci-app-secubox-netdiag/compile V=s
|
||||||
|
|
||||||
|
# Install on device
|
||||||
|
opkg install luci-app-secubox-netdiag_*.ipk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- luci-base
|
||||||
|
- ethtool
|
||||||
|
|
||||||
|
## Menu Location
|
||||||
|
|
||||||
|
SecuBox > Network Diagnostics
|
||||||
|
|
||||||
|
## Error Metrics Monitored
|
||||||
|
|
||||||
|
| Metric | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| rx_crc_errors | CRC/FCS checksum errors |
|
||||||
|
| rx_frame_errors | Framing errors |
|
||||||
|
| rx_fifo_errors | FIFO overrun errors |
|
||||||
|
| rx_missed_errors | Missed packets (buffer full) |
|
||||||
|
| tx_aborted_errors | TX aborts |
|
||||||
|
| tx_carrier_errors | Carrier sense errors |
|
||||||
|
| collisions | Ethernet collisions |
|
||||||
|
| rx_dropped | Receive drops |
|
||||||
|
| tx_dropped | Transmit drops |
|
||||||
|
|
||||||
|
## Alert Thresholds
|
||||||
|
|
||||||
|
| Level | Condition | Indicator |
|
||||||
|
|-------|-----------|-----------|
|
||||||
|
| Normal | 0 errors/minute | Green |
|
||||||
|
| Warning | 1-10 errors/minute | Yellow |
|
||||||
|
| Critical | >10 errors/minute | Red (pulsing) |
|
||||||
|
|
||||||
|
## RPCD API
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
```
|
||||||
|
luci.secubox-netdiag
|
||||||
|
get_switch_status - All interfaces with DSA topology
|
||||||
|
get_interface_details { interface: string } - Full ethtool/dmesg details
|
||||||
|
get_error_history { interface: string, minutes: int } - Error timeline
|
||||||
|
get_topology - DSA switch structure
|
||||||
|
clear_counters { interface: string } - Clear error history
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example ubus call
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ubus call luci.secubox-netdiag get_switch_status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Sources
|
||||||
|
|
||||||
|
- `/sys/class/net/*/statistics/*` - Kernel statistics
|
||||||
|
- `/sys/class/net/*/carrier` - Link state
|
||||||
|
- `/sys/class/net/*/master` - DSA topology
|
||||||
|
- `ethtool <iface>` - Link parameters
|
||||||
|
- `ethtool -S <iface>` - Driver statistics
|
||||||
|
- `dmesg` - Kernel messages
|
||||||
|
|
||||||
|
## UI Components
|
||||||
|
|
||||||
|
### Port Card
|
||||||
|
```
|
||||||
|
+----------+
|
||||||
|
| eth0 |
|
||||||
|
| [*] Up |
|
||||||
|
| 1G FD |
|
||||||
|
| OK |
|
||||||
|
+----------+
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Monitor
|
||||||
|
```
|
||||||
|
eth2 - CRC Errors (last 5 min)
|
||||||
|
[sparkline graph] 123/min (CRITICAL)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
```
|
||||||
|
luci-app-secubox-netdiag/
|
||||||
|
Makefile
|
||||||
|
htdocs/luci-static/resources/
|
||||||
|
view/secubox-netdiag/
|
||||||
|
overview.js # Main LuCI view
|
||||||
|
secubox-netdiag/
|
||||||
|
netdiag.css # SecuBox theme styles
|
||||||
|
root/usr/
|
||||||
|
libexec/rpcd/
|
||||||
|
luci.secubox-netdiag # RPCD backend script
|
||||||
|
share/
|
||||||
|
luci/menu.d/
|
||||||
|
luci-app-secubox-netdiag.json
|
||||||
|
rpcd/acl.d/
|
||||||
|
luci-app-secubox-netdiag.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
### Main Dashboard
|
||||||
|
- DSA switch ports in grid layout
|
||||||
|
- Standalone interfaces below
|
||||||
|
- Error monitor at bottom
|
||||||
|
|
||||||
|
### Port Detail Modal
|
||||||
|
- Link status (speed, duplex, autoneg)
|
||||||
|
- Traffic statistics (bytes, packets)
|
||||||
|
- Error counters with deltas
|
||||||
|
- Recent kernel messages
|
||||||
|
- Clear History / Export Log buttons
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@ -0,0 +1,550 @@
|
|||||||
|
/* SecuBox Network Diagnostics Dashboard Styles */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--sb-primary: #1a5f7a;
|
||||||
|
--sb-primary-light: #2980b9;
|
||||||
|
--sb-success: #57cc99;
|
||||||
|
--sb-warning: #ffca3a;
|
||||||
|
--sb-critical: #ff595e;
|
||||||
|
--sb-bg-dark: #0d1b2a;
|
||||||
|
--sb-bg-card: #1b263b;
|
||||||
|
--sb-bg-card-hover: #243b55;
|
||||||
|
--sb-text: #e0e1dd;
|
||||||
|
--sb-text-muted: #8d99ae;
|
||||||
|
--sb-border: #415a77;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-container {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
background: var(--sb-bg-dark);
|
||||||
|
color: var(--sb-text);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.netdiag-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: linear-gradient(135deg, var(--sb-primary) 0%, var(--sb-primary-light) 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(26, 95, 122, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-title-icon {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-refresh-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-refresh-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border: 1px solid rgba(255,255,255,0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-refresh-btn:hover {
|
||||||
|
background: rgba(255,255,255,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-refresh-select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border: 1px solid rgba(255,255,255,0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-refresh-select option {
|
||||||
|
background: var(--sb-bg-dark);
|
||||||
|
color: var(--sb-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section containers */
|
||||||
|
.netdiag-section {
|
||||||
|
background: var(--sb-bg-card);
|
||||||
|
border: 1px solid var(--sb-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--sb-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-section-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-section-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Port grid */
|
||||||
|
.netdiag-ports-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Port card */
|
||||||
|
.netdiag-port {
|
||||||
|
background: var(--sb-bg-dark);
|
||||||
|
border: 2px solid var(--sb-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-port:hover {
|
||||||
|
background: var(--sb-bg-card-hover);
|
||||||
|
border-color: var(--sb-primary);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-port.port-up {
|
||||||
|
border-color: var(--sb-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-port.port-down {
|
||||||
|
border-color: var(--sb-text-muted);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-port.port-warning {
|
||||||
|
border-color: var(--sb-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-port.port-critical {
|
||||||
|
border-color: var(--sb-critical);
|
||||||
|
animation: pulse-error 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-error {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 89, 94, 0.4); }
|
||||||
|
50% { box-shadow: 0 0 0 8px rgba(255, 89, 94, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-port-name {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-port-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-port-indicator {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-port-indicator.up { background: var(--sb-success); }
|
||||||
|
.netdiag-port-indicator.down { background: var(--sb-text-muted); }
|
||||||
|
.netdiag-port-indicator.warning { background: var(--sb-warning); }
|
||||||
|
.netdiag-port-indicator.critical { background: var(--sb-critical); }
|
||||||
|
|
||||||
|
.netdiag-port-speed {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--sb-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-port-errors {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--sb-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-port-errors.ok { color: var(--sb-success); }
|
||||||
|
.netdiag-port-errors.warning { color: var(--sb-warning); }
|
||||||
|
.netdiag-port-errors.critical { color: var(--sb-critical); }
|
||||||
|
|
||||||
|
/* Error monitor widget */
|
||||||
|
.netdiag-error-monitor {
|
||||||
|
background: var(--sb-bg-dark);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-error-interface {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--sb-bg-card);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-left: 4px solid var(--sb-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-error-interface.warning {
|
||||||
|
border-left-color: var(--sb-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-error-interface.critical {
|
||||||
|
border-left-color: var(--sb-critical);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-error-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-error-iface-name {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-error-stats {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--sb-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-error-rate {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-error-rate.warning { color: var(--sb-warning); }
|
||||||
|
.netdiag-error-rate.critical { color: var(--sb-critical); }
|
||||||
|
|
||||||
|
/* Sparkline */
|
||||||
|
.netdiag-sparkline {
|
||||||
|
width: 120px;
|
||||||
|
height: 32px;
|
||||||
|
margin: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-sparkline svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-sparkline-path {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--sb-warning);
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-sparkline-area {
|
||||||
|
fill: rgba(255, 202, 58, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail modal */
|
||||||
|
.netdiag-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-modal-content {
|
||||||
|
background: var(--sb-bg-card);
|
||||||
|
border: 1px solid var(--sb-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 700px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--sb-border);
|
||||||
|
background: var(--sb-primary);
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-modal-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-modal-close {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-modal-close:hover {
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail sections */
|
||||||
|
.netdiag-detail-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-detail-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--sb-primary-light);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-detail-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-detail-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--sb-bg-dark);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-detail-label {
|
||||||
|
color: var(--sb-text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-detail-value {
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error table in detail */
|
||||||
|
.netdiag-error-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-error-table th,
|
||||||
|
.netdiag-error-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--sb-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-error-table th {
|
||||||
|
color: var(--sb-text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-error-table td.delta-up {
|
||||||
|
color: var(--sb-critical);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-error-table td.delta-up::after {
|
||||||
|
content: ' \25B2';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kernel log */
|
||||||
|
.netdiag-dmesg {
|
||||||
|
background: #000;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-dmesg-line {
|
||||||
|
color: #0f0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-dmesg-line.error {
|
||||||
|
color: var(--sb-critical);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action buttons */
|
||||||
|
.netdiag-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--sb-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-btn-primary {
|
||||||
|
background: var(--sb-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-btn-primary:hover {
|
||||||
|
background: var(--sb-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-btn-secondary {
|
||||||
|
background: var(--sb-bg-dark);
|
||||||
|
color: var(--sb-text);
|
||||||
|
border: 1px solid var(--sb-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-btn-secondary:hover {
|
||||||
|
background: var(--sb-bg-card-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-btn-danger {
|
||||||
|
background: var(--sb-critical);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-btn-danger:hover {
|
||||||
|
background: #e04347;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading state */
|
||||||
|
.netdiag-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: var(--sb-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 3px solid var(--sb-border);
|
||||||
|
border-top-color: var(--sb-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.netdiag-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: var(--sb-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.netdiag-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-ports-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-modal-content {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-modal-header {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-detail-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.netdiag-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.netdiag-ports-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,581 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require rpc';
|
||||||
|
'require poll';
|
||||||
|
|
||||||
|
var callNetdiagStatus = rpc.declare({
|
||||||
|
object: 'luci.secubox-netdiag',
|
||||||
|
method: 'get_switch_status',
|
||||||
|
expect: { ports: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callNetdiagDetails = rpc.declare({
|
||||||
|
object: 'luci.secubox-netdiag',
|
||||||
|
method: 'get_interface_details',
|
||||||
|
params: ['interface'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callNetdiagHistory = rpc.declare({
|
||||||
|
object: 'luci.secubox-netdiag',
|
||||||
|
method: 'get_error_history',
|
||||||
|
params: ['interface', 'minutes'],
|
||||||
|
expect: { timeline: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callNetdiagTopology = rpc.declare({
|
||||||
|
object: 'luci.secubox-netdiag',
|
||||||
|
method: 'get_topology',
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var callClearCounters = rpc.declare({
|
||||||
|
object: 'luci.secubox-netdiag',
|
||||||
|
method: 'clear_counters',
|
||||||
|
params: ['interface'],
|
||||||
|
expect: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
refreshInterval: 5000,
|
||||||
|
pollHandle: null,
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
callNetdiagStatus(),
|
||||||
|
callNetdiagTopology()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var ports = data[0] || [];
|
||||||
|
var topoData = data[1] || {};
|
||||||
|
var topology = topoData.topology || {};
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var container = E('div', { 'class': 'netdiag-container' }, [
|
||||||
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-netdiag/netdiag.css') }),
|
||||||
|
this.renderHeader(),
|
||||||
|
E('div', { 'id': 'netdiag-content' }, [
|
||||||
|
this.renderSwitchSection(ports, topology),
|
||||||
|
this.renderStandaloneSection(ports, topology),
|
||||||
|
this.renderErrorMonitor(ports)
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Start polling
|
||||||
|
this.startPolling();
|
||||||
|
|
||||||
|
return container;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHeader: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return E('div', { 'class': 'netdiag-header' }, [
|
||||||
|
E('h1', { 'class': 'netdiag-title' }, [
|
||||||
|
E('span', { 'class': 'netdiag-title-icon' }, '\uD83D\uDD0C'),
|
||||||
|
_('Network Diagnostics')
|
||||||
|
]),
|
||||||
|
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'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSwitchSection: function(ports, topology) {
|
||||||
|
var self = this;
|
||||||
|
var switches = topology.switches || [];
|
||||||
|
|
||||||
|
// If no DSA topology detected, return empty
|
||||||
|
if (switches.length === 0) {
|
||||||
|
// Check if any ports have a master
|
||||||
|
var dsaPorts = ports.filter(function(p) { return p.is_dsa_port; });
|
||||||
|
if (dsaPorts.length === 0) {
|
||||||
|
return E('div');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group ports by master
|
||||||
|
var portsByMaster = {};
|
||||||
|
ports.forEach(function(port) {
|
||||||
|
if (port.master) {
|
||||||
|
if (!portsByMaster[port.master]) {
|
||||||
|
portsByMaster[port.master] = [];
|
||||||
|
}
|
||||||
|
portsByMaster[port.master].push(port);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var sections = [];
|
||||||
|
Object.keys(portsByMaster).forEach(function(master) {
|
||||||
|
var switchPorts = portsByMaster[master];
|
||||||
|
|
||||||
|
sections.push(E('div', { 'class': 'netdiag-section' }, [
|
||||||
|
E('div', { 'class': 'netdiag-section-header' }, [
|
||||||
|
E('span', { 'class': 'netdiag-section-icon' }, '\uD83D\uDD00'),
|
||||||
|
E('h2', { 'class': 'netdiag-section-title' },
|
||||||
|
_('DSA Switch') + ' (' + master + ')')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'netdiag-ports-grid' },
|
||||||
|
switchPorts.map(function(port) {
|
||||||
|
return self.renderPortCard(port);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
return E('div', { 'id': 'netdiag-switches' }, sections);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderStandaloneSection: function(ports, topology) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Get standalone interfaces (no DSA master)
|
||||||
|
var standalone = ports.filter(function(p) {
|
||||||
|
return !p.is_dsa_port && !p.name.match(/^(br-|lo|docker|veth|tun|tap)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (standalone.length === 0) {
|
||||||
|
return E('div');
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', { 'class': 'netdiag-section', 'id': 'netdiag-standalone' }, [
|
||||||
|
E('div', { 'class': 'netdiag-section-header' }, [
|
||||||
|
E('span', { 'class': 'netdiag-section-icon' }, '\uD83C\uDF10'),
|
||||||
|
E('h2', { 'class': 'netdiag-section-title' }, _('Standalone Interfaces'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'netdiag-ports-grid' },
|
||||||
|
standalone.map(function(port) {
|
||||||
|
return self.renderPortCard(port);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderPortCard: function(port) {
|
||||||
|
var self = this;
|
||||||
|
var link = port.link;
|
||||||
|
var speed = port.speed || 0;
|
||||||
|
var duplex = (port.duplex || '').toLowerCase();
|
||||||
|
var alertLevel = port.alert_level || 'normal';
|
||||||
|
|
||||||
|
var portClass = 'netdiag-port';
|
||||||
|
if (!link) {
|
||||||
|
portClass += ' port-down';
|
||||||
|
} else if (alertLevel === 'critical') {
|
||||||
|
portClass += ' port-critical';
|
||||||
|
} else if (alertLevel === 'warning') {
|
||||||
|
portClass += ' port-warning';
|
||||||
|
} else {
|
||||||
|
portClass += ' port-up';
|
||||||
|
}
|
||||||
|
|
||||||
|
var indicatorClass = 'netdiag-port-indicator';
|
||||||
|
if (!link) {
|
||||||
|
indicatorClass += ' down';
|
||||||
|
} else if (alertLevel === 'critical') {
|
||||||
|
indicatorClass += ' critical';
|
||||||
|
} else if (alertLevel === 'warning') {
|
||||||
|
indicatorClass += ' warning';
|
||||||
|
} else {
|
||||||
|
indicatorClass += ' up';
|
||||||
|
}
|
||||||
|
|
||||||
|
var speedText = '-';
|
||||||
|
if (link && speed > 0) {
|
||||||
|
speedText = (speed >= 1000 ? (speed / 1000) + 'G' : speed + 'M');
|
||||||
|
speedText += ' ' + (duplex === 'full' ? 'FD' : 'HD');
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorText = this.getErrorSummary(port);
|
||||||
|
var errorClass = 'netdiag-port-errors';
|
||||||
|
if (alertLevel === 'critical') {
|
||||||
|
errorClass += ' critical';
|
||||||
|
} else if (alertLevel === 'warning') {
|
||||||
|
errorClass += ' warning';
|
||||||
|
} else {
|
||||||
|
errorClass += ' ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', {
|
||||||
|
'class': portClass,
|
||||||
|
'data-interface': port.name,
|
||||||
|
'click': function() { self.showPortDetails(port.name); }
|
||||||
|
}, [
|
||||||
|
E('div', { 'class': 'netdiag-port-name' }, port.name),
|
||||||
|
E('div', { 'class': 'netdiag-port-status' }, [
|
||||||
|
E('span', { 'class': indicatorClass }),
|
||||||
|
link ? _('Up') : _('Down')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'netdiag-port-speed' }, speedText),
|
||||||
|
E('div', { 'class': errorClass }, errorText)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
getErrorSummary: function(port) {
|
||||||
|
var errors = port.errors || {};
|
||||||
|
var total = 0;
|
||||||
|
|
||||||
|
['rx_crc_errors', 'rx_frame_errors', 'rx_fifo_errors', 'rx_dropped',
|
||||||
|
'tx_dropped', 'collisions'].forEach(function(key) {
|
||||||
|
total += parseInt(errors[key] || 0, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (total === 0) {
|
||||||
|
return '\u2713 OK';
|
||||||
|
}
|
||||||
|
|
||||||
|
var rate = port.error_rate || 0;
|
||||||
|
if (rate > 0) {
|
||||||
|
return '\u26A0 ' + total + ' err (' + rate + '/min)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '\u26A0 ' + total + ' err';
|
||||||
|
},
|
||||||
|
|
||||||
|
renderErrorMonitor: function(ports) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Filter ports with errors
|
||||||
|
var errorPorts = ports.filter(function(p) {
|
||||||
|
return (p.alert_level === 'warning' || p.alert_level === 'critical') ||
|
||||||
|
(p.error_rate && p.error_rate > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by error rate descending
|
||||||
|
errorPorts.sort(function(a, b) {
|
||||||
|
return (b.error_rate || 0) - (a.error_rate || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errorPorts.length === 0) {
|
||||||
|
return E('div', { 'class': 'netdiag-section', 'id': 'netdiag-errors' }, [
|
||||||
|
E('div', { 'class': 'netdiag-section-header' }, [
|
||||||
|
E('span', { 'class': 'netdiag-section-icon' }, '\u26A0\uFE0F'),
|
||||||
|
E('h2', { 'class': 'netdiag-section-title' }, _('Error Monitor'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'netdiag-empty' }, [
|
||||||
|
E('span', {}, '\u2705 '),
|
||||||
|
_('No errors detected on any interface')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', { 'class': 'netdiag-section', 'id': 'netdiag-errors' }, [
|
||||||
|
E('div', { 'class': 'netdiag-section-header' }, [
|
||||||
|
E('span', { 'class': 'netdiag-section-icon' }, '\u26A0\uFE0F'),
|
||||||
|
E('h2', { 'class': 'netdiag-section-title' }, _('Error Monitor'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'netdiag-error-monitor' },
|
||||||
|
errorPorts.slice(0, 5).map(function(port) {
|
||||||
|
return self.renderErrorRow(port);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderErrorRow: function(port) {
|
||||||
|
var self = this;
|
||||||
|
var errors = port.errors || {};
|
||||||
|
var crcErrors = parseInt(errors.rx_crc_errors || 0, 10);
|
||||||
|
var rate = port.error_rate || 0;
|
||||||
|
var alertLevel = port.alert_level || 'normal';
|
||||||
|
|
||||||
|
var rowClass = 'netdiag-error-interface';
|
||||||
|
if (alertLevel === 'critical') rowClass += ' critical';
|
||||||
|
else if (alertLevel === 'warning') rowClass += ' warning';
|
||||||
|
|
||||||
|
var rateClass = 'netdiag-error-rate';
|
||||||
|
if (alertLevel === 'critical') rateClass += ' critical';
|
||||||
|
else if (alertLevel === 'warning') rateClass += ' warning';
|
||||||
|
|
||||||
|
return E('div', { 'class': rowClass }, [
|
||||||
|
E('div', { 'class': 'netdiag-error-info' }, [
|
||||||
|
E('div', { 'class': 'netdiag-error-iface-name' }, port.name),
|
||||||
|
E('div', { 'class': 'netdiag-error-stats' },
|
||||||
|
_('CRC: %d, Frame: %d, FIFO: %d').format(
|
||||||
|
crcErrors,
|
||||||
|
parseInt(errors.rx_frame_errors || 0, 10),
|
||||||
|
parseInt(errors.rx_fifo_errors || 0, 10)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'netdiag-sparkline', 'data-interface': port.name }),
|
||||||
|
E('div', { 'class': rateClass }, rate + '/min'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'netdiag-btn netdiag-btn-secondary',
|
||||||
|
'style': 'padding: 6px 12px; font-size: 0.8rem;',
|
||||||
|
'click': function(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
self.showPortDetails(port.name);
|
||||||
|
}
|
||||||
|
}, _('Details'))
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
showPortDetails: function(iface) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Show loading modal
|
||||||
|
var modal = E('div', { 'class': 'netdiag-modal' }, [
|
||||||
|
E('div', { 'class': 'netdiag-modal-content' }, [
|
||||||
|
E('div', { 'class': 'netdiag-modal-header' }, [
|
||||||
|
E('span', { 'class': 'netdiag-modal-title' }, [
|
||||||
|
'\uD83D\uDD0C ',
|
||||||
|
iface + ' ' + _('Details')
|
||||||
|
]),
|
||||||
|
E('button', {
|
||||||
|
'class': 'netdiag-modal-close',
|
||||||
|
'click': function() { modal.remove(); }
|
||||||
|
}, '\u2715')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'netdiag-modal-body' }, [
|
||||||
|
E('div', { 'class': 'netdiag-loading' }, [
|
||||||
|
E('div', { 'class': 'netdiag-spinner' }),
|
||||||
|
_('Loading interface details...')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
// Fetch details
|
||||||
|
callNetdiagDetails(iface).then(function(details) {
|
||||||
|
var body = modal.querySelector('.netdiag-modal-body');
|
||||||
|
body.innerHTML = '';
|
||||||
|
|
||||||
|
if (details.error) {
|
||||||
|
body.appendChild(E('div', { 'class': 'netdiag-empty' }, details.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.appendChild(self.renderDetailContent(details, iface));
|
||||||
|
}).catch(function(err) {
|
||||||
|
var body = modal.querySelector('.netdiag-modal-body');
|
||||||
|
body.innerHTML = '';
|
||||||
|
body.appendChild(E('div', { 'class': 'netdiag-empty' }, _('Error loading details: ') + err));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderDetailContent: function(details, iface) {
|
||||||
|
var self = this;
|
||||||
|
var ethtool = details.ethtool || {};
|
||||||
|
var stats = details.stats || {};
|
||||||
|
var errors = details.errors || {};
|
||||||
|
var dmesg = details.dmesg || [];
|
||||||
|
var driverInfo = details.driver_info || {};
|
||||||
|
|
||||||
|
return E('div', {}, [
|
||||||
|
// Link Status
|
||||||
|
E('div', { 'class': 'netdiag-detail-section' }, [
|
||||||
|
E('div', { 'class': 'netdiag-detail-title' }, _('Link Status')),
|
||||||
|
E('div', { 'class': 'netdiag-detail-grid' }, [
|
||||||
|
this.renderDetailItem(_('Speed'), ethtool.speed || '-'),
|
||||||
|
this.renderDetailItem(_('Duplex'), ethtool.duplex || '-'),
|
||||||
|
this.renderDetailItem(_('Auto-negotiation'), ethtool.auto_negotiation || '-'),
|
||||||
|
this.renderDetailItem(_('Link Detected'), ethtool.link_detected || '-'),
|
||||||
|
this.renderDetailItem(_('Port'), ethtool.port || '-'),
|
||||||
|
this.renderDetailItem(_('Driver'), driverInfo.driver || '-')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Traffic Statistics
|
||||||
|
E('div', { 'class': 'netdiag-detail-section' }, [
|
||||||
|
E('div', { 'class': 'netdiag-detail-title' }, _('Traffic Statistics')),
|
||||||
|
E('div', { 'class': 'netdiag-detail-grid' }, [
|
||||||
|
this.renderDetailItem(_('RX Bytes'), this.formatBytes(stats.rx_bytes)),
|
||||||
|
this.renderDetailItem(_('TX Bytes'), this.formatBytes(stats.tx_bytes)),
|
||||||
|
this.renderDetailItem(_('RX Packets'), this.formatNumber(stats.rx_packets)),
|
||||||
|
this.renderDetailItem(_('TX Packets'), this.formatNumber(stats.tx_packets)),
|
||||||
|
this.renderDetailItem(_('RX Dropped'), stats.rx_dropped || '0'),
|
||||||
|
this.renderDetailItem(_('TX Dropped'), stats.tx_dropped || '0')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Error Counters
|
||||||
|
E('div', { 'class': 'netdiag-detail-section' }, [
|
||||||
|
E('div', { 'class': 'netdiag-detail-title' }, _('Error Counters')),
|
||||||
|
E('table', { 'class': 'netdiag-error-table' }, [
|
||||||
|
E('thead', {}, [
|
||||||
|
E('tr', {}, [
|
||||||
|
E('th', {}, _('Counter')),
|
||||||
|
E('th', {}, _('Value')),
|
||||||
|
E('th', {}, _('Status'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('tbody', {}, [
|
||||||
|
this.renderErrorRow2('rx_crc_errors', errors.rx_crc_errors),
|
||||||
|
this.renderErrorRow2('rx_frame_errors', errors.rx_frame_errors),
|
||||||
|
this.renderErrorRow2('rx_fifo_errors', errors.rx_fifo_errors),
|
||||||
|
this.renderErrorRow2('rx_missed_errors', errors.rx_missed_errors),
|
||||||
|
this.renderErrorRow2('tx_aborted_errors', errors.tx_aborted_errors),
|
||||||
|
this.renderErrorRow2('tx_carrier_errors', errors.tx_carrier_errors),
|
||||||
|
this.renderErrorRow2('collisions', errors.collisions)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Kernel Messages
|
||||||
|
dmesg.length > 0 ? E('div', { 'class': 'netdiag-detail-section' }, [
|
||||||
|
E('div', { 'class': 'netdiag-detail-title' }, _('Recent Kernel Messages')),
|
||||||
|
E('div', { 'class': 'netdiag-dmesg' },
|
||||||
|
dmesg.slice(-10).map(function(line) {
|
||||||
|
var lineClass = 'netdiag-dmesg-line';
|
||||||
|
if (line.match(/error|fail|bad/i)) {
|
||||||
|
lineClass += ' error';
|
||||||
|
}
|
||||||
|
return E('div', { 'class': lineClass }, line);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]) : E('div'),
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
E('div', { 'class': 'netdiag-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'netdiag-btn netdiag-btn-secondary',
|
||||||
|
'click': function() {
|
||||||
|
self.clearCounters(iface);
|
||||||
|
}
|
||||||
|
}, _('Clear History')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'netdiag-btn netdiag-btn-secondary',
|
||||||
|
'click': function() {
|
||||||
|
self.exportLog(iface, details);
|
||||||
|
}
|
||||||
|
}, _('Export Log'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderDetailItem: function(label, value) {
|
||||||
|
return E('div', { 'class': 'netdiag-detail-item' }, [
|
||||||
|
E('span', { 'class': 'netdiag-detail-label' }, label),
|
||||||
|
E('span', { 'class': 'netdiag-detail-value' }, value)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderErrorRow2: function(name, value) {
|
||||||
|
var val = parseInt(value || 0, 10);
|
||||||
|
var status = val > 0 ? '\u26A0' : '\u2713';
|
||||||
|
var tdClass = val > 0 ? 'delta-up' : '';
|
||||||
|
|
||||||
|
return E('tr', {}, [
|
||||||
|
E('td', {}, name),
|
||||||
|
E('td', { 'class': tdClass }, String(val)),
|
||||||
|
E('td', {}, status)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
formatBytes: function(bytes) {
|
||||||
|
bytes = parseInt(bytes || 0, 10);
|
||||||
|
if (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];
|
||||||
|
},
|
||||||
|
|
||||||
|
formatNumber: function(num) {
|
||||||
|
num = parseInt(num || 0, 10);
|
||||||
|
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
|
||||||
|
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
|
||||||
|
return String(num);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearCounters: function(iface) {
|
||||||
|
var self = this;
|
||||||
|
callClearCounters(iface).then(function(result) {
|
||||||
|
if (result.success) {
|
||||||
|
ui.addNotification(null, E('p', _('History cleared for %s').format(iface)), 'info');
|
||||||
|
self.refreshData();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', result.message), 'warning');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
exportLog: function(iface, details) {
|
||||||
|
var content = 'SecuBox Network Diagnostics Export\n';
|
||||||
|
content += 'Interface: ' + iface + '\n';
|
||||||
|
content += 'Timestamp: ' + new Date().toISOString() + '\n';
|
||||||
|
content += '\n--- Ethtool ---\n';
|
||||||
|
content += JSON.stringify(details.ethtool, null, 2) + '\n';
|
||||||
|
content += '\n--- Statistics ---\n';
|
||||||
|
content += JSON.stringify(details.stats, null, 2) + '\n';
|
||||||
|
content += '\n--- Errors ---\n';
|
||||||
|
content += JSON.stringify(details.errors, null, 2) + '\n';
|
||||||
|
content += '\n--- Kernel Messages ---\n';
|
||||||
|
(details.dmesg || []).forEach(function(line) {
|
||||||
|
content += line + '\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
var blob = new Blob([content], { type: 'text/plain' });
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'netdiag-' + iface + '-' + Date.now() + '.txt';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData: function() {
|
||||||
|
var self = this;
|
||||||
|
var content = document.getElementById('netdiag-content');
|
||||||
|
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
callNetdiagStatus().then(function(ports) {
|
||||||
|
callNetdiagTopology().then(function(topoData) {
|
||||||
|
var topology = (topoData && topoData.topology) ? topoData.topology : {};
|
||||||
|
|
||||||
|
// Update content
|
||||||
|
content.innerHTML = '';
|
||||||
|
content.appendChild(self.renderSwitchSection(ports || [], topology));
|
||||||
|
content.appendChild(self.renderStandaloneSection(ports || [], topology));
|
||||||
|
content.appendChild(self.renderErrorMonitor(ports || []));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
startPolling: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (this.refreshInterval > 0) {
|
||||||
|
this.pollHandle = poll.add(function() {
|
||||||
|
self.refreshData();
|
||||||
|
}, this.refreshInterval / 1000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
restartPolling: function() {
|
||||||
|
if (this.pollHandle) {
|
||||||
|
poll.remove(this.pollHandle);
|
||||||
|
this.pollHandle = null;
|
||||||
|
}
|
||||||
|
this.startPolling();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,512 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SecuBox Network Diagnostics RPCD Backend
|
||||||
|
# Provides DSA switch port statistics and error monitoring
|
||||||
|
|
||||||
|
. /usr/share/libubox/jshn.sh
|
||||||
|
|
||||||
|
# Error history storage (in-memory via temp files)
|
||||||
|
HISTORY_DIR="/tmp/secubox-netdiag"
|
||||||
|
HISTORY_INTERVAL=5
|
||||||
|
HISTORY_SAMPLES=60 # 5 minutes at 5-second intervals
|
||||||
|
|
||||||
|
# Ensure history directory exists
|
||||||
|
mkdir -p "$HISTORY_DIR" 2>/dev/null
|
||||||
|
|
||||||
|
# Helper: Read a sysfs statistic file
|
||||||
|
read_stat() {
|
||||||
|
local iface="$1"
|
||||||
|
local stat="$2"
|
||||||
|
local path="/sys/class/net/${iface}/statistics/${stat}"
|
||||||
|
if [ -f "$path" ]; then
|
||||||
|
cat "$path" 2>/dev/null || echo "0"
|
||||||
|
else
|
||||||
|
echo "0"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Get interface link state
|
||||||
|
get_link_state() {
|
||||||
|
local iface="$1"
|
||||||
|
local carrier="/sys/class/net/${iface}/carrier"
|
||||||
|
local operstate="/sys/class/net/${iface}/operstate"
|
||||||
|
|
||||||
|
if [ -f "$carrier" ] && [ "$(cat "$carrier" 2>/dev/null)" = "1" ]; then
|
||||||
|
echo "up"
|
||||||
|
elif [ -f "$operstate" ]; then
|
||||||
|
cat "$operstate" 2>/dev/null
|
||||||
|
else
|
||||||
|
echo "unknown"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Get DSA master interface
|
||||||
|
get_dsa_master() {
|
||||||
|
local iface="$1"
|
||||||
|
local master="/sys/class/net/${iface}/master"
|
||||||
|
if [ -L "$master" ]; then
|
||||||
|
basename "$(readlink "$master")" 2>/dev/null
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Get speed and duplex via ethtool
|
||||||
|
get_ethtool_info() {
|
||||||
|
local iface="$1"
|
||||||
|
local result
|
||||||
|
|
||||||
|
result=$(ethtool "$iface" 2>/dev/null)
|
||||||
|
if [ -n "$result" ]; then
|
||||||
|
local speed duplex autoneg link
|
||||||
|
speed=$(echo "$result" | grep -i "Speed:" | awk '{print $2}' | sed 's/Mb\/s//')
|
||||||
|
duplex=$(echo "$result" | grep -i "Duplex:" | awk '{print $2}')
|
||||||
|
autoneg=$(echo "$result" | grep -i "Auto-negotiation:" | awk '{print $2}')
|
||||||
|
link=$(echo "$result" | grep -i "Link detected:" | awk '{print $3}')
|
||||||
|
|
||||||
|
json_add_int "speed" "${speed:-0}"
|
||||||
|
json_add_string "duplex" "${duplex:-unknown}"
|
||||||
|
json_add_string "autoneg" "${autoneg:-unknown}"
|
||||||
|
json_add_string "link_detected" "${link:-unknown}"
|
||||||
|
else
|
||||||
|
json_add_int "speed" 0
|
||||||
|
json_add_string "duplex" "unknown"
|
||||||
|
json_add_string "autoneg" "unknown"
|
||||||
|
json_add_string "link_detected" "unknown"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Get all interface statistics
|
||||||
|
get_interface_stats() {
|
||||||
|
local iface="$1"
|
||||||
|
|
||||||
|
json_add_object "stats"
|
||||||
|
json_add_string "rx_bytes" "$(read_stat "$iface" rx_bytes)"
|
||||||
|
json_add_string "tx_bytes" "$(read_stat "$iface" tx_bytes)"
|
||||||
|
json_add_string "rx_packets" "$(read_stat "$iface" rx_packets)"
|
||||||
|
json_add_string "tx_packets" "$(read_stat "$iface" tx_packets)"
|
||||||
|
json_add_string "rx_dropped" "$(read_stat "$iface" rx_dropped)"
|
||||||
|
json_add_string "tx_dropped" "$(read_stat "$iface" tx_dropped)"
|
||||||
|
json_close_object
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Get error counters
|
||||||
|
get_error_stats() {
|
||||||
|
local iface="$1"
|
||||||
|
|
||||||
|
json_add_object "errors"
|
||||||
|
json_add_string "rx_crc_errors" "$(read_stat "$iface" rx_crc_errors)"
|
||||||
|
json_add_string "rx_frame_errors" "$(read_stat "$iface" rx_frame_errors)"
|
||||||
|
json_add_string "rx_fifo_errors" "$(read_stat "$iface" rx_fifo_errors)"
|
||||||
|
json_add_string "rx_missed_errors" "$(read_stat "$iface" rx_missed_errors)"
|
||||||
|
json_add_string "rx_length_errors" "$(read_stat "$iface" rx_length_errors)"
|
||||||
|
json_add_string "rx_over_errors" "$(read_stat "$iface" rx_over_errors)"
|
||||||
|
json_add_string "tx_aborted_errors" "$(read_stat "$iface" tx_aborted_errors)"
|
||||||
|
json_add_string "tx_carrier_errors" "$(read_stat "$iface" tx_carrier_errors)"
|
||||||
|
json_add_string "tx_fifo_errors" "$(read_stat "$iface" tx_fifo_errors)"
|
||||||
|
json_add_string "tx_heartbeat_errors" "$(read_stat "$iface" tx_heartbeat_errors)"
|
||||||
|
json_add_string "tx_window_errors" "$(read_stat "$iface" tx_window_errors)"
|
||||||
|
json_add_string "collisions" "$(read_stat "$iface" collisions)"
|
||||||
|
json_close_object
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Get ARP/neighbor info for connected devices
|
||||||
|
get_connected_device() {
|
||||||
|
local iface="$1"
|
||||||
|
local neighbor
|
||||||
|
|
||||||
|
# Check ARP table for devices on this interface
|
||||||
|
neighbor=$(ip neigh show dev "$iface" 2>/dev/null | grep -v "FAILED" | head -1)
|
||||||
|
if [ -n "$neighbor" ]; then
|
||||||
|
local ip mac
|
||||||
|
ip=$(echo "$neighbor" | awk '{print $1}')
|
||||||
|
mac=$(echo "$neighbor" | grep -oE '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' | head -1)
|
||||||
|
json_add_object "neighbor"
|
||||||
|
json_add_string "ip" "${ip:-}"
|
||||||
|
json_add_string "mac" "${mac:-}"
|
||||||
|
json_close_object
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Store error history sample
|
||||||
|
store_error_sample() {
|
||||||
|
local iface="$1"
|
||||||
|
local history_file="$HISTORY_DIR/${iface}.history"
|
||||||
|
local timestamp=$(date +%s)
|
||||||
|
|
||||||
|
# Collect current error values
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Append to history file
|
||||||
|
echo "$timestamp $rx_crc $rx_frame $rx_fifo $rx_dropped $tx_dropped $collisions" >> "$history_file"
|
||||||
|
|
||||||
|
# Keep only last HISTORY_SAMPLES entries
|
||||||
|
if [ -f "$history_file" ]; then
|
||||||
|
tail -n "$HISTORY_SAMPLES" "$history_file" > "${history_file}.tmp"
|
||||||
|
mv "${history_file}.tmp" "$history_file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate error rate (errors per minute)
|
||||||
|
calc_error_rate() {
|
||||||
|
local iface="$1"
|
||||||
|
local history_file="$HISTORY_DIR/${iface}.history"
|
||||||
|
local now=$(date +%s)
|
||||||
|
local one_minute_ago=$((now - 60))
|
||||||
|
|
||||||
|
if [ ! -f "$history_file" ]; then
|
||||||
|
echo "0"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get oldest and newest samples within last minute
|
||||||
|
local first_sample last_sample
|
||||||
|
first_sample=$(awk -v t="$one_minute_ago" '$1 >= t {print; exit}' "$history_file")
|
||||||
|
last_sample=$(tail -1 "$history_file")
|
||||||
|
|
||||||
|
if [ -z "$first_sample" ] || [ -z "$last_sample" ]; then
|
||||||
|
echo "0"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Calculate delta for rx_crc_errors (column 2)
|
||||||
|
local first_crc=$(echo "$first_sample" | awk '{print $2}')
|
||||||
|
local last_crc=$(echo "$last_sample" | awk '{print $2}')
|
||||||
|
local delta=$((last_crc - first_crc))
|
||||||
|
|
||||||
|
[ "$delta" -lt 0 ] && delta=0
|
||||||
|
echo "$delta"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get error history for sparkline
|
||||||
|
get_error_history() {
|
||||||
|
local iface="$1"
|
||||||
|
local minutes="${2:-5}"
|
||||||
|
local history_file="$HISTORY_DIR/${iface}.history"
|
||||||
|
local now=$(date +%s)
|
||||||
|
local start_time=$((now - minutes * 60))
|
||||||
|
|
||||||
|
json_add_array "timeline"
|
||||||
|
|
||||||
|
if [ -f "$history_file" ]; then
|
||||||
|
local prev_crc=0
|
||||||
|
local first=1
|
||||||
|
|
||||||
|
while read -r line; do
|
||||||
|
local ts=$(echo "$line" | awk '{print $1}')
|
||||||
|
[ "$ts" -lt "$start_time" ] && continue
|
||||||
|
|
||||||
|
local rx_crc=$(echo "$line" | awk '{print $2}')
|
||||||
|
local rx_frame=$(echo "$line" | awk '{print $3}')
|
||||||
|
local rx_fifo=$(echo "$line" | awk '{print $4}')
|
||||||
|
|
||||||
|
# Calculate delta from previous sample
|
||||||
|
local delta_crc=0
|
||||||
|
if [ "$first" = "0" ]; then
|
||||||
|
delta_crc=$((rx_crc - prev_crc))
|
||||||
|
[ "$delta_crc" -lt 0 ] && delta_crc=0
|
||||||
|
fi
|
||||||
|
first=0
|
||||||
|
prev_crc="$rx_crc"
|
||||||
|
|
||||||
|
json_add_object ""
|
||||||
|
json_add_int "timestamp" "$ts"
|
||||||
|
json_add_int "rx_crc_errors" "$delta_crc"
|
||||||
|
json_add_int "rx_crc_total" "$rx_crc"
|
||||||
|
json_add_int "rx_frame_errors" "$rx_frame"
|
||||||
|
json_add_int "rx_fifo_errors" "$rx_fifo"
|
||||||
|
json_close_object
|
||||||
|
done < "$history_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: get_switch_status
|
||||||
|
# Returns status of all network interfaces with DSA topology
|
||||||
|
get_switch_status() {
|
||||||
|
json_init
|
||||||
|
json_add_array "ports"
|
||||||
|
|
||||||
|
# Iterate all network interfaces
|
||||||
|
for iface_path in /sys/class/net/*; do
|
||||||
|
[ ! -d "$iface_path" ] && continue
|
||||||
|
|
||||||
|
local iface=$(basename "$iface_path")
|
||||||
|
|
||||||
|
# Skip virtual/loopback interfaces
|
||||||
|
case "$iface" in
|
||||||
|
lo|br-*|docker*|veth*|tun*|tap*) continue ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Store error sample for history
|
||||||
|
store_error_sample "$iface"
|
||||||
|
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "name" "$iface"
|
||||||
|
|
||||||
|
# Check if this is a DSA port
|
||||||
|
local master=$(get_dsa_master "$iface")
|
||||||
|
json_add_string "master" "$master"
|
||||||
|
json_add_boolean "is_dsa_port" "$([ -n "$master" ] && echo 1 || echo 0)"
|
||||||
|
|
||||||
|
# Link state
|
||||||
|
local link_state=$(get_link_state "$iface")
|
||||||
|
json_add_string "operstate" "$link_state"
|
||||||
|
json_add_boolean "link" "$([ "$link_state" = "up" ] && echo 1 || echo 0)"
|
||||||
|
|
||||||
|
# Speed/duplex from ethtool
|
||||||
|
get_ethtool_info "$iface"
|
||||||
|
|
||||||
|
# Traffic statistics
|
||||||
|
get_interface_stats "$iface"
|
||||||
|
|
||||||
|
# Error counters
|
||||||
|
get_error_stats "$iface"
|
||||||
|
|
||||||
|
# Error rate (errors/minute)
|
||||||
|
local error_rate=$(calc_error_rate "$iface")
|
||||||
|
json_add_int "error_rate" "$error_rate"
|
||||||
|
|
||||||
|
# Alert level based on error rate
|
||||||
|
local alert_level="normal"
|
||||||
|
[ "$error_rate" -gt 0 ] && [ "$error_rate" -le 10 ] && alert_level="warning"
|
||||||
|
[ "$error_rate" -gt 10 ] && alert_level="critical"
|
||||||
|
json_add_string "alert_level" "$alert_level"
|
||||||
|
|
||||||
|
# Connected device info
|
||||||
|
get_connected_device "$iface"
|
||||||
|
|
||||||
|
# MAC address
|
||||||
|
local mac=""
|
||||||
|
[ -f "/sys/class/net/${iface}/address" ] && mac=$(cat "/sys/class/net/${iface}/address" 2>/dev/null)
|
||||||
|
json_add_string "mac" "${mac:-}"
|
||||||
|
|
||||||
|
# MTU
|
||||||
|
local mtu=""
|
||||||
|
[ -f "/sys/class/net/${iface}/mtu" ] && mtu=$(cat "/sys/class/net/${iface}/mtu" 2>/dev/null)
|
||||||
|
json_add_int "mtu" "${mtu:-1500}"
|
||||||
|
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: get_interface_details
|
||||||
|
# Returns detailed information for a specific interface
|
||||||
|
get_interface_details() {
|
||||||
|
local iface="$1"
|
||||||
|
|
||||||
|
# Validate interface exists
|
||||||
|
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"
|
||||||
|
|
||||||
|
# Full ethtool output
|
||||||
|
json_add_object "ethtool"
|
||||||
|
local ethtool_out=$(ethtool "$iface" 2>/dev/null)
|
||||||
|
if [ -n "$ethtool_out" ]; then
|
||||||
|
# Parse key fields
|
||||||
|
json_add_string "speed" "$(echo "$ethtool_out" | grep -i 'Speed:' | awk '{print $2}')"
|
||||||
|
json_add_string "duplex" "$(echo "$ethtool_out" | grep -i 'Duplex:' | awk '{print $2}')"
|
||||||
|
json_add_string "auto_negotiation" "$(echo "$ethtool_out" | grep -i 'Auto-negotiation:' | awk '{print $2}')"
|
||||||
|
json_add_string "link_detected" "$(echo "$ethtool_out" | grep -i 'Link detected:' | awk '{print $3}')"
|
||||||
|
json_add_string "port" "$(echo "$ethtool_out" | grep -i 'Port:' | cut -d: -f2 | xargs)"
|
||||||
|
json_add_string "transceiver" "$(echo "$ethtool_out" | grep -i 'Transceiver:' | awk '{print $2}')"
|
||||||
|
|
||||||
|
# Supported modes
|
||||||
|
local modes=$(echo "$ethtool_out" | grep -A20 'Supported link modes:' | grep -E '^\s+[0-9]+' | tr '\n' ' ')
|
||||||
|
json_add_string "supported_modes" "$modes"
|
||||||
|
|
||||||
|
# Link partner
|
||||||
|
local partner=$(echo "$ethtool_out" | grep -A5 'Link partner' | grep -E '^\s+[0-9]+' | tr '\n' ' ')
|
||||||
|
json_add_string "link_partner" "$partner"
|
||||||
|
fi
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
# Extended statistics (ethtool -S)
|
||||||
|
json_add_object "driver_stats"
|
||||||
|
local ethtool_s=$(ethtool -S "$iface" 2>/dev/null)
|
||||||
|
if [ -n "$ethtool_s" ]; then
|
||||||
|
echo "$ethtool_s" | grep -E '^\s+[a-z_]+:' | while read -r line; do
|
||||||
|
local key=$(echo "$line" | cut -d: -f1 | xargs)
|
||||||
|
local val=$(echo "$line" | cut -d: -f2 | xargs)
|
||||||
|
# Only include first 50 stats to avoid huge output
|
||||||
|
[ -n "$key" ] && [ -n "$val" ] && json_add_string "$key" "$val"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
# Driver info (ethtool -i)
|
||||||
|
json_add_object "driver_info"
|
||||||
|
local ethtool_i=$(ethtool -i "$iface" 2>/dev/null)
|
||||||
|
if [ -n "$ethtool_i" ]; then
|
||||||
|
json_add_string "driver" "$(echo "$ethtool_i" | grep 'driver:' | cut -d: -f2 | xargs)"
|
||||||
|
json_add_string "version" "$(echo "$ethtool_i" | grep 'version:' | cut -d: -f2 | xargs)"
|
||||||
|
json_add_string "firmware" "$(echo "$ethtool_i" | grep 'firmware-version:' | cut -d: -f2 | xargs)"
|
||||||
|
json_add_string "bus_info" "$(echo "$ethtool_i" | grep 'bus-info:' | cut -d: -f2 | xargs)"
|
||||||
|
fi
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
# Recent kernel messages
|
||||||
|
json_add_array "dmesg"
|
||||||
|
dmesg 2>/dev/null | grep -i "$iface" | tail -20 | while read -r line; do
|
||||||
|
json_add_string "" "$line"
|
||||||
|
done
|
||||||
|
json_close_array
|
||||||
|
|
||||||
|
# Current stats
|
||||||
|
get_interface_stats "$iface"
|
||||||
|
get_error_stats "$iface"
|
||||||
|
|
||||||
|
# Error history
|
||||||
|
get_error_history "$iface" 5
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: get_error_history (standalone)
|
||||||
|
get_error_history_method() {
|
||||||
|
local iface="$1"
|
||||||
|
local minutes="${2:-5}"
|
||||||
|
|
||||||
|
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"
|
||||||
|
get_error_history "$iface" "$minutes"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: clear_counters
|
||||||
|
# Clear error history (counters are read-only in sysfs)
|
||||||
|
clear_counters() {
|
||||||
|
local iface="$1"
|
||||||
|
local history_file="$HISTORY_DIR/${iface}.history"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ -n "$iface" ] && [ -f "$history_file" ]; then
|
||||||
|
rm -f "$history_file"
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Cleared history for $iface"
|
||||||
|
elif [ -z "$iface" ]; then
|
||||||
|
rm -f "$HISTORY_DIR"/*.history
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Cleared all history"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "message" "No history found for $iface"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method: get_topology
|
||||||
|
# Returns DSA switch topology
|
||||||
|
get_topology() {
|
||||||
|
json_init
|
||||||
|
json_add_object "topology"
|
||||||
|
|
||||||
|
# Find DSA master interfaces
|
||||||
|
json_add_array "switches"
|
||||||
|
|
||||||
|
for master_path in /sys/class/net/*/dsa; do
|
||||||
|
[ ! -d "$master_path" ] && continue
|
||||||
|
local master=$(dirname "$master_path" | xargs basename)
|
||||||
|
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "master" "$master"
|
||||||
|
json_add_string "driver" "$(cat /sys/class/net/${master}/device/driver/module/name 2>/dev/null || echo 'unknown')"
|
||||||
|
|
||||||
|
# Find ports belonging to this master
|
||||||
|
json_add_array "ports"
|
||||||
|
for port_path in /sys/class/net/*; do
|
||||||
|
[ ! -d "$port_path" ] && continue
|
||||||
|
local port=$(basename "$port_path")
|
||||||
|
local port_master=$(get_dsa_master "$port")
|
||||||
|
|
||||||
|
if [ "$port_master" = "$master" ]; then
|
||||||
|
json_add_string "" "$port"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
json_close_array
|
||||||
|
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
|
||||||
|
# Standalone interfaces (not DSA ports, not virtual)
|
||||||
|
json_add_array "standalone"
|
||||||
|
for iface_path in /sys/class/net/*; do
|
||||||
|
[ ! -d "$iface_path" ] && continue
|
||||||
|
local iface=$(basename "$iface_path")
|
||||||
|
local master=$(get_dsa_master "$iface")
|
||||||
|
|
||||||
|
# Skip if has DSA master or is virtual
|
||||||
|
[ -n "$master" ] && continue
|
||||||
|
case "$iface" in
|
||||||
|
lo|br-*|docker*|veth*|tun*|tap*) continue ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check if it's a real ethernet device
|
||||||
|
[ -f "/sys/class/net/${iface}/device" ] || [ -d "/sys/class/net/${iface}/device" ] && {
|
||||||
|
json_add_string "" "$iface"
|
||||||
|
}
|
||||||
|
done
|
||||||
|
json_close_array
|
||||||
|
|
||||||
|
json_close_object
|
||||||
|
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":{}}'
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
get_switch_status)
|
||||||
|
get_switch_status
|
||||||
|
;;
|
||||||
|
get_interface_details)
|
||||||
|
read -r input
|
||||||
|
iface=$(echo "$input" | jsonfilter -e '@.interface' 2>/dev/null)
|
||||||
|
get_interface_details "$iface"
|
||||||
|
;;
|
||||||
|
get_error_history)
|
||||||
|
read -r input
|
||||||
|
iface=$(echo "$input" | jsonfilter -e '@.interface' 2>/dev/null)
|
||||||
|
minutes=$(echo "$input" | jsonfilter -e '@.minutes' 2>/dev/null)
|
||||||
|
get_error_history_method "$iface" "${minutes:-5}"
|
||||||
|
;;
|
||||||
|
clear_counters)
|
||||||
|
read -r input
|
||||||
|
iface=$(echo "$input" | jsonfilter -e '@.interface' 2>/dev/null)
|
||||||
|
clear_counters "$iface"
|
||||||
|
;;
|
||||||
|
get_topology)
|
||||||
|
get_topology
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"admin/secubox/netdiag": {
|
||||||
|
"title": "Network Diagnostics",
|
||||||
|
"order": 45,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "secubox-netdiag/overview"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": ["luci-app-secubox-netdiag"],
|
||||||
|
"uci": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"luci-app-secubox-netdiag": {
|
||||||
|
"description": "Grant access to SecuBox Network Diagnostics",
|
||||||
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.secubox-netdiag": ["get_switch_status", "get_interface_details", "get_error_history", "get_topology"]
|
||||||
|
},
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.secubox-netdiag": ["clear_counters"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,7 +19,8 @@ var appCategories = [
|
|||||||
description: 'SecuBox administration and app management',
|
description: 'SecuBox administration and app management',
|
||||||
apps: [
|
apps: [
|
||||||
{ id: 'secubox-admin', name: 'SecuBox Admin', icon: '\ud83d\udcbb', path: 'admin/secubox/admin', desc: 'App catalog, updates, and system configuration' },
|
{ id: 'secubox-admin', name: 'SecuBox Admin', icon: '\ud83d\udcbb', path: 'admin/secubox/admin', desc: 'App catalog, updates, and system configuration' },
|
||||||
{ id: 'cyber-dashboard', name: 'Cyber Dashboard', icon: '\ud83d\udcca', path: 'admin/secubox/admin/cyber-dashboard', desc: 'Advanced analytics and insights' }
|
{ id: 'cyber-dashboard', name: 'Cyber Dashboard', icon: '\ud83d\udcca', path: 'admin/secubox/admin/cyber-dashboard', desc: 'Advanced analytics and insights' },
|
||||||
|
{ id: 'netdiag', name: 'Network Diagnostics', icon: '\ud83d\udd0c', path: 'admin/secubox/netdiag', desc: 'DSA switch port statistics and error monitoring' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user