mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-09-14 12:24:20 +08:00
4179 lines
146 KiB
HTML
4179 lines
146 KiB
HTML
<link rel="stylesheet" href="<%=resource%>/oaf/css/common.css">
|
|
<script src="<%=resource%>/oaf/jquery-3.7.1.min.js"></script>
|
|
<script src="<%=resource%>/oaf/oaf_icon.js"></script>
|
|
<script src="<%=resource%>/oaf/echarts.min.js"></script>
|
|
|
|
<style>
|
|
:root {
|
|
--chart-text-color: #333333;
|
|
--chart-axis-line-color: rgba(0, 0, 0, 0.2);
|
|
--chart-axis-label-color: #666666;
|
|
--chart-split-line-color: rgba(0, 0, 0, 0.08);
|
|
--chart-split-line-dashed-color: rgba(0, 0, 0, 0.15);
|
|
--chart-tooltip-bg: rgba(255, 255, 255, 0.95);
|
|
--chart-tooltip-text: #333333;
|
|
--temp-bar-default-gradient: linear-gradient(90deg,#d9eefc,#ff9800);
|
|
--temp-space-bar-gradient: linear-gradient(90deg,#22c4df,#4ea6f6);
|
|
--storage-bar-gradient: linear-gradient(90deg,#1fc2de,#4ca6fb);
|
|
--storage-bar-shadow: 0 0 10px rgba(66, 214, 255, 0.34);
|
|
}
|
|
|
|
[data-darkmode="true"] {
|
|
--temp-space-bar-gradient: linear-gradient(90deg,#a1ecff,#abd5ff);
|
|
--storage-bar-gradient: linear-gradient(90deg,#9deaff,#a6ceff);
|
|
--storage-bar-shadow: 0 0 14px rgba(151, 217, 255, 0.58);
|
|
}
|
|
|
|
.dash {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) minmax(0, 340px);
|
|
gap: 12px;
|
|
padding: 0 0 8px;
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
min-width: 0;
|
|
}
|
|
.dash > * {
|
|
min-width: 0;
|
|
}
|
|
.card {
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(121, 141, 164, 0.36);
|
|
padding: 4px 12px;
|
|
min-height: 200px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
[data-darkmode="true"] .card {
|
|
border-color: rgba(255, 255, 255, 0.24);
|
|
}
|
|
#active-app-card {
|
|
min-height: auto;
|
|
}
|
|
.card h3 { margin: 0 0 2px 0; font-size: 14px; }
|
|
.card > .mini-title,
|
|
.card .card-title-row > .mini-title {
|
|
margin: 0 0 2px 0;
|
|
padding: 0;
|
|
border: 0;
|
|
background: transparent;
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
color: inherit;
|
|
}
|
|
.chart-container {
|
|
flex: 1;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
min-width: 0;
|
|
min-height: 250px;
|
|
height: 35vh;
|
|
max-height: 450px;
|
|
overflow: hidden;
|
|
box-sizing: border-box;
|
|
}
|
|
#history-traffic-card {
|
|
min-height: 270px;
|
|
}
|
|
#history-traffic-card .chart-container {
|
|
min-height: 220px;
|
|
height: 260px;
|
|
max-height: 300px;
|
|
}
|
|
#pie-app-card, #bar-user-traffic-card {
|
|
min-height: 470px;
|
|
}
|
|
#pie-app-card .chart-container, #bar-user-traffic-card .chart-container {
|
|
min-height: 400px;
|
|
height: 100%;
|
|
max-height: none;
|
|
}
|
|
.tab-btn {
|
|
padding: 4px 12px;
|
|
font-size: 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.app-stats-switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
}
|
|
.mode-switch-guide-bubble {
|
|
position: fixed;
|
|
z-index: 9999;
|
|
width: 340px;
|
|
max-width: calc(100vw - 16px);
|
|
padding: 14px 16px 12px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(37, 99, 235, 0.28);
|
|
background: linear-gradient(135deg, rgba(239, 246, 255, 0.98), rgba(219, 234, 254, 0.98));
|
|
color: #1e40af;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
box-shadow: 0 16px 32px rgba(29, 78, 216, 0.22);
|
|
pointer-events: auto;
|
|
}
|
|
.mode-switch-guide-bubble::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: -7px;
|
|
right: 18px;
|
|
width: 12px;
|
|
height: 12px;
|
|
background: rgba(231, 242, 255, 0.98);
|
|
border-left: 1px solid rgba(37, 99, 235, 0.28);
|
|
border-top: 1px solid rgba(37, 99, 235, 0.28);
|
|
transform: rotate(45deg);
|
|
}
|
|
.mode-switch-guide-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
margin-bottom: 6px;
|
|
color: #1e3a8a;
|
|
}
|
|
.mode-switch-guide-text {
|
|
margin: 0;
|
|
}
|
|
.mode-switch-guide-actions {
|
|
margin-top: 10px;
|
|
text-align: right;
|
|
}
|
|
.mode-switch-guide-ok {
|
|
border: 1px solid rgba(37, 99, 235, 0.35);
|
|
background: #2563eb;
|
|
color: #fff;
|
|
border-radius: 8px;
|
|
padding: 6px 14px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
line-height: 1.2;
|
|
}
|
|
.mode-switch-guide-ok:hover {
|
|
background: #1d4ed8;
|
|
}
|
|
[data-darkmode="true"] .mode-switch-guide-bubble {
|
|
border-color: rgba(147, 197, 253, 0.32);
|
|
background: linear-gradient(135deg, rgba(30, 41, 59, 0.98), rgba(30, 64, 175, 0.35));
|
|
color: #dbeafe;
|
|
box-shadow: 0 16px 36px rgba(2, 6, 23, 0.55);
|
|
}
|
|
[data-darkmode="true"] .mode-switch-guide-bubble::before {
|
|
background: rgba(34, 53, 90, 0.98);
|
|
border-left-color: rgba(147, 197, 253, 0.32);
|
|
border-top-color: rgba(147, 197, 253, 0.32);
|
|
}
|
|
[data-darkmode="true"] .mode-switch-guide-title {
|
|
color: #eff6ff;
|
|
}
|
|
[data-darkmode="true"] .mode-switch-guide-ok {
|
|
border-color: rgba(191, 219, 254, 0.35);
|
|
background: #3b82f6;
|
|
color: #eff6ff;
|
|
}
|
|
[data-darkmode="true"] .mode-switch-guide-ok:hover {
|
|
background: #2563eb;
|
|
}
|
|
.app-stats-select-wrap {
|
|
display: none !important;
|
|
}
|
|
.app-stats-switch-btn {
|
|
min-width: 78px;
|
|
height: 22px;
|
|
font-size: 12px;
|
|
line-height: 20px;
|
|
border-radius: 4px;
|
|
padding: 0 18px 0 8px;
|
|
border: 1px solid rgba(148,163,184,.45);
|
|
background: transparent;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
}
|
|
.app-stats-switch-btn::after {
|
|
content: "";
|
|
position: absolute;
|
|
right: 7px;
|
|
top: 7px;
|
|
width: 6px;
|
|
height: 6px;
|
|
border-right: 1.5px solid currentColor;
|
|
border-bottom: 1.5px solid currentColor;
|
|
transform: rotate(45deg);
|
|
opacity: .75;
|
|
}
|
|
.app-stats-switch-menu {
|
|
display: none;
|
|
position: absolute;
|
|
right: 0;
|
|
top: 24px;
|
|
min-width: 78px;
|
|
z-index: 20;
|
|
border-radius: 4px;
|
|
border: 1px solid rgba(148,163,184,.45);
|
|
overflow: hidden;
|
|
background: #ffffff;
|
|
color: #1f2937;
|
|
}
|
|
.app-stats-switch.open .app-stats-switch-menu {
|
|
display: block;
|
|
}
|
|
.app-stats-switch-item {
|
|
display: block;
|
|
width: 100%;
|
|
border: 0;
|
|
background: transparent;
|
|
color: inherit;
|
|
text-align: left;
|
|
font-size: 12px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
}
|
|
.app-stats-switch-item:hover,
|
|
.app-stats-switch-item.active {
|
|
background: #e5e7eb;
|
|
color: #111827;
|
|
}
|
|
[data-darkmode="true"] .app-stats-switch-menu {
|
|
background: #1f2937;
|
|
color: #e5e7eb;
|
|
border-color: #374151;
|
|
}
|
|
[data-darkmode="true"] .app-stats-switch-item:hover,
|
|
[data-darkmode="true"] .app-stats-switch-item.active {
|
|
background: #374151;
|
|
color: #f3f4f6;
|
|
}
|
|
.tab-btn:hover {
|
|
}
|
|
.tab-btn.active {
|
|
background: rgba(59, 130, 246, 0.3);
|
|
border-color: rgba(59, 130, 246, 0.5);
|
|
color: #3b82f6;
|
|
}
|
|
.top-stats { display:grid; grid-template-columns: repeat(4,1fr); gap:10px; margin-bottom:12px; }
|
|
.acceleration-warning {
|
|
display: none;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
margin: 0 0 10px;
|
|
padding: 10px 14px;
|
|
border: 1px solid rgba(245, 158, 11, 0.42);
|
|
border-radius: 7px;
|
|
background: rgba(245, 158, 11, 0.10);
|
|
color: #b45309;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
.acceleration-warning-text {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.acceleration-warning-close {
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 0;
|
|
border-radius: 50%;
|
|
background: transparent;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
font-size: 18px;
|
|
line-height: 18px;
|
|
padding: 0;
|
|
opacity: .72;
|
|
flex: none;
|
|
}
|
|
.acceleration-warning-close:hover {
|
|
opacity: 1;
|
|
background: rgba(245, 158, 11, 0.16);
|
|
}
|
|
.acceleration-warning strong {
|
|
font-weight: 600;
|
|
}
|
|
[data-darkmode="true"] .acceleration-warning {
|
|
border-color: rgba(251, 191, 36, 0.44);
|
|
background: rgba(120, 53, 15, 0.30);
|
|
color: #fcd34d;
|
|
}
|
|
.top-stats .big { font-size:15px; font-weight:600; }
|
|
.top-stats .top-stat-value {
|
|
font-size: 21px;
|
|
font-weight: 700;
|
|
line-height: 1.05;
|
|
letter-spacing: 0.01em;
|
|
color: #eaf2fb;
|
|
text-shadow: 0 1px 0 rgba(5, 12, 22, 0.35);
|
|
}
|
|
.mini {
|
|
border-radius: 8px;
|
|
padding: 15px 10px;
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.mini > div:first-child {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
.top-stats .mini {
|
|
--stat-bg-from: #222c38;
|
|
--stat-bg-mid: #1c2530;
|
|
--stat-bg-to: #18212b;
|
|
--stat-glow-color: rgba(90, 167, 248, 0.32);
|
|
--stat-icon-color: #7fb4f5;
|
|
position: relative;
|
|
overflow: hidden;
|
|
border-radius: 12px;
|
|
padding: 11px 13px 12px;
|
|
gap: 10px;
|
|
min-height: 88px;
|
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
|
box-shadow: 0 10px 22px rgba(8, 15, 30, 0.30), 0 2px 6px rgba(8, 15, 30, 0.16);
|
|
background:
|
|
radial-gradient(circle at 88% 12%, var(--stat-glow-color) 0%, rgba(255, 255, 255, 0) 46%),
|
|
linear-gradient(160deg, var(--stat-bg-from) 0%, var(--stat-bg-mid) 55%, var(--stat-bg-to) 100%);
|
|
color: rgba(248, 251, 255, 0.98);
|
|
}
|
|
.top-stats .mini:nth-child(2) {
|
|
--stat-glow-color: rgba(163, 148, 255, 0.30);
|
|
--stat-icon-color: #a394ff;
|
|
}
|
|
.top-stats .mini:nth-child(3) {
|
|
--stat-glow-color: rgba(69, 215, 155, 0.30);
|
|
--stat-icon-color: #4fd09a;
|
|
}
|
|
.top-stats .mini:nth-child(4) {
|
|
--stat-glow-color: rgba(247, 181, 107, 0.30);
|
|
--stat-icon-color: #f2ae66;
|
|
}
|
|
.top-stats .mini:nth-child(1),
|
|
.top-stats .mini:nth-child(2),
|
|
.top-stats .mini:nth-child(3) {
|
|
cursor: pointer;
|
|
overflow: visible;
|
|
}
|
|
.top-stats .mini > .tooltip {
|
|
position: absolute;
|
|
z-index: 1000;
|
|
}
|
|
#oaf-feature-version,
|
|
#oaf-app-record-count {
|
|
cursor: pointer;
|
|
}
|
|
.top-stats .stat-head {
|
|
position: relative;
|
|
display: block;
|
|
min-height: 24px;
|
|
padding-right: 30px;
|
|
}
|
|
.top-stats .stat-label {
|
|
display: block;
|
|
font-size: 12px;
|
|
line-height: 24px;
|
|
padding-left: 0;
|
|
font-weight: 600;
|
|
letter-spacing: 0.05em;
|
|
text-transform: uppercase;
|
|
opacity: 0.86;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.top-stats .stat-icon {
|
|
position: absolute;
|
|
top: 2px;
|
|
right: 0;
|
|
left: auto;
|
|
width: 22px;
|
|
height: 22px;
|
|
display: block;
|
|
background-color: var(--stat-icon-color);
|
|
opacity: 0.92;
|
|
-webkit-mask: var(--stat-icon-url) center / contain no-repeat;
|
|
mask: var(--stat-icon-url) center / contain no-repeat;
|
|
}
|
|
.top-stats .icon-application { --stat-icon-url: url('/luci-static/resources/oaf/icons/application.svg'); }
|
|
.top-stats .icon-user { --stat-icon-url: url('/luci-static/resources/oaf/icons/user.svg'); }
|
|
.top-stats .icon-connect { --stat-icon-url: url('/luci-static/resources/oaf/icons/filter.svg'); }
|
|
.top-stats .icon-flow { --stat-icon-url: url('/luci-static/resources/oaf/icons/flow.svg'); }
|
|
.top-stats .mini .top-stat-value {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.top-stats #mini-traffic {
|
|
font-size: 19px;
|
|
}
|
|
.top-stats .mini > * {
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
[data-darkmode="true"] .top-stats .mini {
|
|
border-color: rgba(255, 255, 255, 0.16);
|
|
box-shadow: 0 10px 22px rgba(2, 8, 20, 0.34), 0 2px 6px rgba(2, 8, 20, 0.18);
|
|
}
|
|
:root:not([data-darkmode="true"]) .top-stats .mini {
|
|
--stat-bg-from: #ffffff;
|
|
--stat-bg-mid: #f8fafc;
|
|
--stat-bg-to: #f1f5f9;
|
|
--stat-glow-color: rgba(90, 167, 248, 0.16);
|
|
--stat-icon-color: #3d7fc4;
|
|
color: #1f2d3d;
|
|
border-color: rgba(121, 141, 164, 0.24);
|
|
box-shadow: 0 10px 20px rgba(20, 26, 36, 0.12), 0 2px 5px rgba(20, 26, 36, 0.06);
|
|
}
|
|
:root:not([data-darkmode="true"]) .top-stats .mini:nth-child(2) {
|
|
--stat-glow-color: rgba(139, 124, 246, 0.14);
|
|
--stat-icon-color: #6b5ae2;
|
|
}
|
|
:root:not([data-darkmode="true"]) .top-stats .mini:nth-child(3) {
|
|
--stat-glow-color: rgba(47, 191, 127, 0.14);
|
|
--stat-icon-color: #1f9c68;
|
|
}
|
|
:root:not([data-darkmode="true"]) .top-stats .mini:nth-child(4) {
|
|
--stat-glow-color: rgba(224, 148, 74, 0.14);
|
|
--stat-icon-color: #cf7f2f;
|
|
}
|
|
:root:not([data-darkmode="true"]) .top-stats .top-stat-value {
|
|
color: #172638;
|
|
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.32);
|
|
}
|
|
:root:not([data-darkmode="true"]) .top-stats .stat-label {
|
|
color: rgba(31, 45, 62, 0.86);
|
|
opacity: 0.88;
|
|
}
|
|
.kv { display: grid; grid-template-columns: 90px 1fr; row-gap: 6px; column-gap: 4px; font-size: 13px; }
|
|
.mini-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
margin-bottom: 4px;
|
|
}
|
|
.mini-title.with-help {
|
|
position: relative;
|
|
cursor: help;
|
|
}
|
|
.compact-mini {
|
|
gap: 8px;
|
|
}
|
|
.compact-kv {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
.compact-row {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
padding: 7px 0;
|
|
border-radius: 0;
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
.compact-row::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
height: 1px;
|
|
background: linear-gradient(
|
|
90deg,
|
|
rgba(148, 163, 184, 0) 0,
|
|
rgba(148, 163, 184, 0.18) 33%,
|
|
rgba(148, 163, 184, 0.18) 67%,
|
|
rgba(148, 163, 184, 0) 100%
|
|
);
|
|
}
|
|
[data-darkmode="true"] .compact-row {
|
|
background: transparent;
|
|
}
|
|
[data-darkmode="true"] .compact-row::after {
|
|
background: linear-gradient(
|
|
90deg,
|
|
rgba(120, 138, 162, 0) 0,
|
|
rgba(120, 138, 162, 0.22) 33%,
|
|
rgba(120, 138, 162, 0.22) 67%,
|
|
rgba(120, 138, 162, 0) 100%
|
|
);
|
|
}
|
|
.compact-kv .compact-row:last-child::after {
|
|
display: none;
|
|
}
|
|
.compact-label {
|
|
font-size: 12px;
|
|
opacity: 0.78;
|
|
white-space: nowrap;
|
|
}
|
|
.compact-value {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
[data-darkmode="true"] .compact-label {
|
|
color: rgba(184, 199, 220, 0.78);
|
|
}
|
|
[data-darkmode="true"] .compact-value {
|
|
color: #dbe9fb;
|
|
}
|
|
[data-darkmode="true"] #sys-model {
|
|
color: #f2f8ff;
|
|
text-shadow: 0 0 0 rgba(0, 0, 0, 0.01);
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
text-rendering: geometricPrecision;
|
|
}
|
|
#sys-hostname,
|
|
#sys-model,
|
|
#sys-version,
|
|
#sys-kernel-version,
|
|
#sys-uptime {
|
|
max-width: 170px;
|
|
}
|
|
.env-row {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
padding: 2px 0;
|
|
}
|
|
.env-row + .env-row {
|
|
margin-top: 10px;
|
|
}
|
|
.env-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
font-size: 13px;
|
|
}
|
|
.env-label {
|
|
font-weight: 600;
|
|
opacity: 0.86;
|
|
}
|
|
.env-value {
|
|
font-weight: 600;
|
|
}
|
|
#env-storage-card .env-label,
|
|
#env-storage-card .env-value {
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
text-rendering: geometricPrecision;
|
|
opacity: 1;
|
|
letter-spacing: 0.1px;
|
|
}
|
|
[data-darkmode="true"] #env-storage-card .env-label {
|
|
color: rgba(215, 228, 244, 0.92);
|
|
}
|
|
[data-darkmode="true"] #env-storage-card .env-value {
|
|
color: #ecf6ff;
|
|
}
|
|
#sys-model, #sys-cpu-model-name {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.meter { height: 6px; background: rgba(0, 0, 0, 0.08); border-radius: 999px; overflow: hidden; position: relative; }
|
|
|
|
[data-darkmode="true"] .meter { background: #141d2a; }
|
|
.bar { height: 100%; width: 0; background: linear-gradient(90deg,#16a34a,#22c55e); position: relative; overflow: hidden; border-radius: 999px; }
|
|
#cpu-temp-bar {
|
|
background: var(--temp-bar-default-gradient);
|
|
}
|
|
#storage-overlay-bar {
|
|
background: var(--storage-bar-gradient);
|
|
box-shadow: var(--storage-bar-shadow);
|
|
}
|
|
#port-status-card {
|
|
gap: 8px;
|
|
}
|
|
.port-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, 1fr);
|
|
gap: 8px;
|
|
}
|
|
.port-item {
|
|
position: relative;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 4px;
|
|
color: var(--text-color-medium, #6b7280);
|
|
}
|
|
.port-icon {
|
|
width: 34px;
|
|
height: 34px;
|
|
color: #6b7280;
|
|
background: currentColor;
|
|
-webkit-mask: url("<%=resource%>/oaf/icons/ethernet-port.svg") center / contain no-repeat;
|
|
mask: url("<%=resource%>/oaf/icons/ethernet-port.svg") center / contain no-repeat;
|
|
}
|
|
.port-item.port-up.port-speed-high .port-icon {
|
|
color: #f1d950;
|
|
}
|
|
.port-item.port-up.port-speed-mid .port-icon {
|
|
color: #18b12c;
|
|
}
|
|
.port-item.port-up.port-speed-low .port-icon {
|
|
color: #08ecec;
|
|
}
|
|
.port-name {
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
line-height: 1.1;
|
|
}
|
|
.port-speed {
|
|
font-size: 10px;
|
|
line-height: 1;
|
|
opacity: 0.78;
|
|
}
|
|
.port-empty {
|
|
color: var(--text-color-medium, #6b7280);
|
|
text-align: center;
|
|
padding: 8px 0 4px;
|
|
}
|
|
.port-item:hover .tooltip {
|
|
display: block;
|
|
}
|
|
|
|
.sys-info-layout {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto;
|
|
gap: 20px;
|
|
align-items: start;
|
|
}
|
|
.sys-info-text {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.sys-info-text-item {
|
|
font-size: 13px;
|
|
line-height: 1.6;
|
|
}
|
|
.sys-info-text-label {
|
|
margin-right: 6px;
|
|
}
|
|
.sys-info-text-value {
|
|
font-weight: 500;
|
|
}
|
|
.sys-info-circulars {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0;
|
|
}
|
|
|
|
.circular-progress {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
}
|
|
.circular-progress-wrapper {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 85px;
|
|
height: 85px;
|
|
}
|
|
.circular-progress svg {
|
|
width: 85px;
|
|
height: 85px;
|
|
transform: rotate(-90deg);
|
|
}
|
|
.circular-progress-label {
|
|
margin-top: 10px;
|
|
font-size: 13px;
|
|
text-align: center;
|
|
}
|
|
.circular-progress .progress-ring-bg {
|
|
fill: none;
|
|
stroke: rgba(0, 0, 0, 0.1);
|
|
stroke-width: 7;
|
|
}
|
|
|
|
[data-darkmode="true"] .circular-progress .progress-ring-bg {
|
|
stroke: rgba(132, 153, 182, 0.22);
|
|
}
|
|
.circular-progress .progress-ring {
|
|
fill: none;
|
|
stroke-width: 7;
|
|
stroke-linecap: round;
|
|
transition: stroke-dashoffset 0.5s ease;
|
|
}
|
|
.circular-progress.cpu .progress-ring {
|
|
stroke: url(#cpuGradient);
|
|
}
|
|
.circular-progress.mem .progress-ring {
|
|
stroke: url(#memGradient);
|
|
}
|
|
.circular-progress-text {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
.progress-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
margin-left: 12px;
|
|
}
|
|
.progress-info-label {
|
|
font-size: 12px;
|
|
margin-bottom: 2px;
|
|
}
|
|
.progress-info-value {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
.main-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; min-width: 0; }
|
|
.full { grid-column: 1 / -1; }
|
|
.list {
|
|
font-size: 13px;
|
|
min-height: 300px;
|
|
}
|
|
.list table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
|
.list th, .list td { border-bottom: 1px solid var(--border-color-low); padding: 6px 4px; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.list th:nth-child(1), .list td:nth-child(1) { width: 14%; }
|
|
.list th:nth-child(2), .list td:nth-child(2) { width: 10%; }
|
|
.list th:nth-child(3), .list td:nth-child(3) { width: 10%; }
|
|
.list th:nth-child(4), .list td:nth-child(4) { width: 10%; }
|
|
.list th:nth-child(5), .list td:nth-child(5) { width: 12%; }
|
|
.list th:nth-child(6), .list td:nth-child(6) { width: 12%; }
|
|
.list th:nth-child(7), .list td:nth-child(7) { width: 12%; }
|
|
.list th:nth-child(8), .list td:nth-child(8) { width: 20%; }
|
|
.dashboard-device-info { display:flex; align-items:center; gap:6px; min-width:0; }
|
|
.dashboard-device-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 6px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 0 0 24px;
|
|
color: #fff;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
border: 1px solid rgba(148, 163, 184, 0.25);
|
|
box-sizing: border-box;
|
|
}
|
|
.dashboard-device-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
|
|
.right-col { display: flex; flex-direction: column; gap: 10px; min-width: 0; max-width: 100%; }
|
|
.right-col .mini {
|
|
position: relative;
|
|
border: 1px solid rgba(121, 141, 164, 0.36);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
padding-left: 13px;
|
|
padding-right: 13px;
|
|
}
|
|
#sys-info-card,
|
|
#active-terminal-card,
|
|
#port-status-card {
|
|
overflow: visible;
|
|
}
|
|
.right-col .mini-title {
|
|
font-weight: 400;
|
|
}
|
|
.right-col .compact-value {
|
|
font-weight: 400;
|
|
}
|
|
.right-col .env-label,
|
|
.right-col .env-value {
|
|
font-weight: 400;
|
|
}
|
|
.right-col .circular-progress-text {
|
|
font-weight: 400;
|
|
}
|
|
#sys-info-card .compact-kv {
|
|
gap: 0;
|
|
}
|
|
#sys-info-card .compact-row {
|
|
padding-top: 6px;
|
|
padding-bottom: 6px;
|
|
}
|
|
#active-terminal-list {
|
|
/* Tune active terminal columns here. Total width + 3 gaps should fit the right card content width. */
|
|
--terminal-name-width: 86px;
|
|
--terminal-up-width: 66px;
|
|
--terminal-down-width: 66px;
|
|
--terminal-app-width: 78px;
|
|
--terminal-column-gap: 6px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
height: 238px;
|
|
overflow: visible;
|
|
}
|
|
.active-terminal-item {
|
|
display: grid;
|
|
grid-template-columns:
|
|
var(--terminal-name-width)
|
|
var(--terminal-up-width)
|
|
var(--terminal-down-width)
|
|
var(--terminal-app-width);
|
|
align-items: center;
|
|
gap: var(--terminal-column-gap);
|
|
padding: 5px 0;
|
|
border-bottom: 1px solid rgba(148, 163, 184, 0.18);
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
min-width: 0;
|
|
min-height: 28px;
|
|
box-sizing: border-box;
|
|
position: relative;
|
|
}
|
|
.active-terminal-item:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
.active-terminal-name {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: inherit;
|
|
}
|
|
.active-terminal-app {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
opacity: 0.82;
|
|
min-width: 0;
|
|
}
|
|
.active-terminal-up {
|
|
color: #58a6ff;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
min-width: 0;
|
|
}
|
|
.active-terminal-down {
|
|
color: #8b7bff;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
min-width: 0;
|
|
}
|
|
.active-terminal-empty {
|
|
padding: 12px 0;
|
|
text-align: center;
|
|
color: #888;
|
|
font-size: 12px;
|
|
}
|
|
.active-terminal-item > .tooltip {
|
|
position: absolute;
|
|
z-index: 1000;
|
|
max-width: 320px;
|
|
right: 100%;
|
|
margin-right: 8px;
|
|
left: auto;
|
|
top: 0;
|
|
transform: none;
|
|
}
|
|
.active-terminal-item:hover .tooltip {
|
|
display: block;
|
|
}
|
|
.active-terminal-item .tooltip-value {
|
|
word-break: break-all;
|
|
}
|
|
#active-terminal-card:hover {
|
|
z-index: 100;
|
|
}
|
|
[data-darkmode="true"] .right-col .mini {
|
|
background: rgba(255,255,255,0.03);
|
|
border: 1px solid rgba(255, 255, 255, 0.24);
|
|
box-shadow: none;
|
|
}
|
|
[data-darkmode="true"] .right-col .mini-title {
|
|
color: #e5eefc;
|
|
}
|
|
[data-darkmode="true"] #running-status-card .circular-progress-text {
|
|
color: #e6f0ff;
|
|
}
|
|
[data-darkmode="true"] #running-status-card .circular-progress-label {
|
|
color: rgba(221, 234, 250, 0.92);
|
|
}
|
|
.spark { height: 70px; }
|
|
.app-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(10, 1fr);
|
|
gap: 12px;
|
|
padding: 4px 0;
|
|
min-height: 60px;
|
|
}
|
|
.app-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
position: relative;
|
|
cursor: pointer;
|
|
}
|
|
.app-icon {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: inherit;
|
|
object-fit: cover;
|
|
display: block;
|
|
opacity: 1;
|
|
}
|
|
.app-icon-letter {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: inherit;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
color: #ffffff;
|
|
line-height: 1;
|
|
user-select: none;
|
|
}
|
|
.app-icon-wrap {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 8px;
|
|
margin-bottom: 6px;
|
|
overflow: hidden;
|
|
display: inline-block;
|
|
vertical-align: middle;
|
|
border: 1px solid rgba(148, 163, 184, 0.25);
|
|
box-sizing: border-box;
|
|
}
|
|
[data-darkmode="true"] .app-icon-wrap {
|
|
border-color: rgba(148, 163, 184, 0.35);
|
|
}
|
|
[data-darkmode="true"] .app-icon {
|
|
filter: none;
|
|
}
|
|
.app-name {
|
|
width: 4.8em;
|
|
max-width: 100%;
|
|
font-size: 12px;
|
|
line-height: 1.3;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.active-url-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0;
|
|
padding: 0;
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
position: relative;
|
|
justify-content: flex-start;
|
|
}
|
|
#active-url-card {
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
.active-url-item {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 10px 0;
|
|
}
|
|
.active-url-item::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
height: 1px;
|
|
background: linear-gradient(
|
|
90deg,
|
|
rgba(148, 163, 184, 0) 0,
|
|
rgba(148, 163, 184, 0.18) 33%,
|
|
rgba(148, 163, 184, 0.18) 67%,
|
|
rgba(148, 163, 184, 0) 100%
|
|
);
|
|
}
|
|
.active-url-item.slide-in {
|
|
animation: slideInFromBottom 0.4s ease-out;
|
|
}
|
|
@keyframes slideInFromBottom {
|
|
from {
|
|
transform: translateY(100%);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateY(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
[data-darkmode="true"] .active-url-item::after {
|
|
background: linear-gradient(
|
|
90deg,
|
|
rgba(120, 138, 162, 0) 0,
|
|
rgba(120, 138, 162, 0.22) 33%,
|
|
rgba(120, 138, 162, 0.22) 67%,
|
|
rgba(120, 138, 162, 0) 100%
|
|
);
|
|
}
|
|
.active-url-item:last-child::after {
|
|
display: none;
|
|
}
|
|
.active-url-item {
|
|
width: 100%;
|
|
min-width: 0;
|
|
box-sizing: border-box;
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
}
|
|
.active-url-item.www-highlight {
|
|
}
|
|
.active-url-name {
|
|
flex: 0 0 auto;
|
|
max-width: 9em;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-weight: 500;
|
|
margin-right: 6px;
|
|
}
|
|
.active-url-url {
|
|
display: flex;
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
}
|
|
.active-url-url-prefix {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.active-url-url-suffix {
|
|
flex: 0 0 auto;
|
|
white-space: nowrap;
|
|
}
|
|
.active-url-item.www-highlight .active-url-url {
|
|
color: #22c55e;
|
|
}
|
|
.tooltip {
|
|
--tooltip-bg: rgba(20, 29, 41, 0.96);
|
|
--tooltip-border: rgba(118, 136, 160, 0.34);
|
|
--tooltip-label-color: rgba(194, 210, 228, 0.78);
|
|
--tooltip-value-color: rgba(229, 238, 250, 0.94);
|
|
position: absolute;
|
|
background: var(--tooltip-bg);
|
|
border: 1px solid var(--tooltip-border);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
min-width: 280px;
|
|
font-size: 12px;
|
|
line-height: 1.6;
|
|
z-index: 1000;
|
|
pointer-events: none;
|
|
box-shadow: 0 10px 24px rgba(6, 12, 22, 0.40);
|
|
display: none;
|
|
}
|
|
:root:not([data-darkmode="true"]) .tooltip {
|
|
--tooltip-bg: #ffffff;
|
|
--tooltip-border: rgba(151, 166, 183, 0.42);
|
|
--tooltip-label-color: rgba(90, 106, 124, 0.86);
|
|
--tooltip-value-color: rgba(32, 45, 62, 0.95);
|
|
box-shadow: 0 8px 20px rgba(40, 51, 66, 0.16);
|
|
}
|
|
.top-stats .mini > .tooltip {
|
|
min-width: 190px;
|
|
}
|
|
|
|
.tooltip-top {
|
|
bottom: 100%;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.tooltip-bottom {
|
|
top: 100%;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.tooltip-left {
|
|
right: 100%;
|
|
margin-right: 8px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
}
|
|
|
|
.tooltip-right {
|
|
left: 100%;
|
|
margin-left: 8px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
}
|
|
|
|
.tooltip-top::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 100%;
|
|
border: 6px solid transparent;
|
|
border-top-color: var(--tooltip-bg);
|
|
}
|
|
|
|
.tooltip-arrow-left::before {
|
|
left: 20px;
|
|
}
|
|
|
|
.tooltip-arrow-center::before {
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.tooltip-center-x {
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.tooltip-top.tooltip-center-x.tooltip-arrow-center::before {
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.tooltip-arrow-right::before {
|
|
right: 20px;
|
|
left: auto;
|
|
}
|
|
|
|
|
|
.tooltip-bottom::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 100%;
|
|
border: 6px solid transparent;
|
|
border-bottom-color: var(--tooltip-bg);
|
|
}
|
|
|
|
.tooltip-bottom.tooltip-arrow-center::after {
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
}
|
|
.tooltip-bottom.tooltip-arrow-left::after {
|
|
left: 20px;
|
|
}
|
|
.tooltip-bottom.tooltip-arrow-right::after {
|
|
right: 20px;
|
|
left: auto;
|
|
}
|
|
|
|
.tooltip-row {
|
|
display: flex;
|
|
margin-bottom: 6px;
|
|
}
|
|
.tooltip-row:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
.tooltip-label {
|
|
color: var(--tooltip-label-color);
|
|
min-width: 90px;
|
|
margin-right: 8px;
|
|
}
|
|
.tooltip-value {
|
|
color: var(--tooltip-value-color);
|
|
flex: 1;
|
|
}
|
|
|
|
.app-item:hover .tooltip {
|
|
display: block;
|
|
}
|
|
|
|
|
|
.dash {
|
|
min-width: 0;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.dash {
|
|
grid-template-columns: 1fr;
|
|
gap: 12px;
|
|
padding: 0 0 6px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.card {
|
|
padding: 10px;
|
|
min-height: auto;
|
|
}
|
|
|
|
.card h3 {
|
|
font-size: 14px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.chart-container {
|
|
min-height: 200px;
|
|
height: 250px;
|
|
max-height: 300px;
|
|
}
|
|
|
|
#pie-app-card, #bar-user-traffic-card {
|
|
min-height: 350px;
|
|
}
|
|
#pie-app-card .chart-container, #bar-user-traffic-card .chart-container {
|
|
min-height: 300px;
|
|
height: 300px;
|
|
}
|
|
|
|
.top-stats {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 8px;
|
|
}
|
|
|
|
.top-stats .big {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.top-stats .mini {
|
|
min-height: 80px;
|
|
padding: 9px 10px 10px;
|
|
gap: 8px;
|
|
}
|
|
|
|
.top-stats .stat-label {
|
|
font-size: 11px;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.top-stats .stat-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
|
|
.top-stats .top-stat-value {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.top-stats #mini-traffic {
|
|
font-size: 15px;
|
|
}
|
|
|
|
.main-grid {
|
|
grid-template-columns: 1fr;
|
|
gap: 10px;
|
|
}
|
|
|
|
.sys-info-layout {
|
|
grid-template-columns: 1fr;
|
|
gap: 15px;
|
|
}
|
|
|
|
.mini {
|
|
padding: 12px 8px;
|
|
font-size: 11px;
|
|
}
|
|
.right-col .mini {
|
|
padding-left: 11px;
|
|
padding-right: 11px;
|
|
}
|
|
|
|
.kv {
|
|
grid-template-columns: 80px 1fr;
|
|
font-size: 12px;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="dash">
|
|
<div>
|
|
<div class="acceleration-warning" id="acceleration-warning">
|
|
<div class="acceleration-warning-text">
|
|
<strong><%:Tip%>:</strong>
|
|
<%:Some modules may affect filtering and identification. If it does not work, please disable related functions such as software and hardware acceleration, ad filtering, mwan3, QoS, etc.%>
|
|
</div>
|
|
<button type="button" class="acceleration-warning-close" id="acceleration-warning-close" title="<%:Close%>" aria-label="<%:Close%>">×</button>
|
|
</div>
|
|
<div class="top-stats">
|
|
<div class="mini">
|
|
<div class="stat-head">
|
|
<span class="stat-label"><%:Online App%></span>
|
|
<span class="stat-icon icon-application" aria-hidden="true"></span>
|
|
</div>
|
|
<div id="app-count" class="top-stat-value">0</div>
|
|
<span class="tooltip">
|
|
<div class="tooltip-row"><span class="tooltip-label"><%:Online App%>:</span><span class="tooltip-value" id="app-online-count">0</span></div>
|
|
<div class="tooltip-row"><span class="tooltip-label"><%:Offline App%>:</span><span class="tooltip-value" id="app-offline-count">0</span></div>
|
|
</span>
|
|
</div>
|
|
<div class="mini">
|
|
<div class="stat-head">
|
|
<span class="stat-label"><%:Online User%></span>
|
|
<span class="stat-icon icon-user" aria-hidden="true"></span>
|
|
</div>
|
|
<div id="mini-online" class="top-stat-value">0</div>
|
|
<span class="tooltip">
|
|
<div class="tooltip-row"><span class="tooltip-label"><%:Online User%>:</span><span class="tooltip-value" id="user-online-count">0</span></div>
|
|
<div class="tooltip-row"><span class="tooltip-label"><%:Offline User%>:</span><span class="tooltip-value" id="user-offline-count">0</span></div>
|
|
</span>
|
|
</div>
|
|
<div class="mini">
|
|
<div class="stat-head">
|
|
<span class="stat-label"><%:Filter Rules%></span>
|
|
<span class="stat-icon icon-connect" aria-hidden="true"></span>
|
|
</div>
|
|
<div id="mini-rule-count" class="top-stat-value">0</div>
|
|
<span class="tooltip">
|
|
<div class="tooltip-row"><span class="tooltip-label"><%:App Filter Rules%>:</span><span class="tooltip-value" id="rule-af-count">0</span></div>
|
|
<div class="tooltip-row"><span class="tooltip-label"><%:Access Control Rules%>:</span><span class="tooltip-value" id="rule-mf-count">0</span></div>
|
|
</span>
|
|
</div>
|
|
<div class="mini">
|
|
<div class="stat-head">
|
|
<span class="stat-label"><%:Today Traffic%></span>
|
|
<span class="stat-icon icon-flow" aria-hidden="true"></span>
|
|
</div>
|
|
<div id="mini-traffic" class="top-stat-value">0MB</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-grid">
|
|
|
|
|
|
<!--
|
|
<div class="card full">
|
|
<h3 id="traffic-title"><%:Real-time Traffic%></h3>
|
|
<div id="line-wan" class="chart-container"></div>
|
|
<div id="wan-realtime-rate" style="text-align: center; margin-top: 8px; font-size: 13px;">
|
|
<span><%:Upload%>:</span><span id="wan-up-rate">0 KB/s</span>
|
|
<span style="margin-left: 15px;"><%:Download%>:</span><span id="wan-down-rate">0 KB/s</span>
|
|
</div>
|
|
</div>
|
|
-->
|
|
<div class="card full" id="history-traffic-card">
|
|
<div class="card-title-row" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
|
|
<div class="mini-title"><%:Traffic Statistics%></div>
|
|
<span class="app-stats-select-wrap">
|
|
<select id="traffic-stats-select" class="app-stats-select">
|
|
<option value="today" selected><%:Today%></option>
|
|
<option value="days30"><%:Last 30 Days%></option>
|
|
</select>
|
|
</span>
|
|
</div>
|
|
<div id="history-traffic-chart" class="chart-container"></div>
|
|
</div>
|
|
<div class="card full" id="active-app-card">
|
|
<div class="mini-title"><%:Visiting%></div>
|
|
<div class="app-grid" id="active-apps"></div>
|
|
</div>
|
|
<div class="card" id="pie-app-card">
|
|
<div class="card-title-row" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
|
|
<div class="mini-title"><%:App Duration Statistics%></div>
|
|
<span class="app-stats-select-wrap">
|
|
<select id="app-stats-select" class="app-stats-select">
|
|
<option value="hourly"><%:Last Hour%></option>
|
|
<option value="daily" selected><%:Today%></option>
|
|
</select>
|
|
</span>
|
|
</div>
|
|
<div id="pie-app" class="chart-container"></div>
|
|
</div>
|
|
<div class="card" id="bar-user-traffic-card">
|
|
<div class="mini-title"><%:User Traffic Top8%></div>
|
|
<div id="bar-user-traffic" class="chart-container"></div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="right-col">
|
|
<div class="mini compact-mini" id="sys-info-card" style="position:relative;">
|
|
<div class="mini-title" id="oaf-status-title"><%:OAF Status%></div>
|
|
<div class="compact-kv">
|
|
<div class="compact-row">
|
|
<span class="compact-label"><%:Work Mode%></span>
|
|
<span class="compact-value" id="oaf-work-mode">--</span>
|
|
</div>
|
|
<div class="compact-row">
|
|
<span class="compact-label"><%:Service Version%></span>
|
|
<span class="compact-value" id="oaf-version">--</span>
|
|
</div>
|
|
<div class="compact-row">
|
|
<span class="compact-label"><%:Engine Version%></span>
|
|
<span class="compact-value" id="oaf-engine-version">--</span>
|
|
</div>
|
|
<div class="compact-row">
|
|
<span class="compact-label"><%:Feature Library%></span>
|
|
<span class="compact-value" id="oaf-feature-version">--</span>
|
|
</div>
|
|
<div class="compact-row">
|
|
<span class="compact-label"><%:App Record Count%></span>
|
|
<span class="compact-value" id="oaf-app-record-count">--</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mini" id="running-status-card" style="display:none;">
|
|
<div style="font-weight:400; margin-bottom:10px;"><%:Running Status%></div>
|
|
|
|
<svg style="position:absolute; width:0; height:0;">
|
|
<defs>
|
|
<linearGradient id="cpuGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#26f7de;stop-opacity:1" />
|
|
<stop offset="100%" style="stop-color:#17d8ff;stop-opacity:1" />
|
|
</linearGradient>
|
|
<linearGradient id="memGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#74e3ff;stop-opacity:1" />
|
|
<stop offset="100%" style="stop-color:#56b9ff;stop-opacity:1" />
|
|
</linearGradient>
|
|
</defs>
|
|
</svg>
|
|
|
|
<div style="display:flex; gap:20px; justify-content:space-around; align-items:flex-start;">
|
|
<div class="circular-progress cpu">
|
|
<div class="circular-progress-wrapper">
|
|
<svg>
|
|
<circle class="progress-ring-bg" cx="42.5" cy="42.5" r="35"></circle>
|
|
<circle class="progress-ring" cx="42.5" cy="42.5" r="35" id="cpu-ring"></circle>
|
|
</svg>
|
|
<div class="circular-progress-text" id="cpu-text">0%</div>
|
|
</div>
|
|
<div class="circular-progress-label">CPU</div>
|
|
</div>
|
|
<div class="circular-progress mem">
|
|
<div class="circular-progress-wrapper">
|
|
<svg>
|
|
<circle class="progress-ring-bg" cx="42.5" cy="42.5" r="35"></circle>
|
|
<circle class="progress-ring" cx="42.5" cy="42.5" r="35" id="mem-ring"></circle>
|
|
</svg>
|
|
<div class="circular-progress-text" id="mem-text">0%</div>
|
|
</div>
|
|
<div class="circular-progress-label"><%:Memory%></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mini compact-mini" id="active-terminal-card" style="position:relative;">
|
|
<div class="mini-title"><%:Active Terminals%></div>
|
|
<div id="active-terminal-list"></div>
|
|
</div>
|
|
<div class="mini compact-mini" id="port-status-card" style="display:none;">
|
|
<div class="mini-title"><%:Port Status%></div>
|
|
<div class="port-grid" id="port-status-grid"></div>
|
|
</div>
|
|
<div class="mini compact-mini" id="env-storage-card" style="display:none;">
|
|
<div class="env-row">
|
|
<div class="env-head">
|
|
<span class="env-label" id="env-primary-label"><%:Temperature%></span>
|
|
<span class="env-value" id="cpu-temp-text">--</span>
|
|
</div>
|
|
<div class="meter">
|
|
<div id="cpu-temp-bar" class="bar"></div>
|
|
</div>
|
|
</div>
|
|
<div class="env-row">
|
|
<div class="env-head">
|
|
<span class="env-label"><%:Disk Space%></span>
|
|
<span class="env-value" id="storage-overlay-text">--</span>
|
|
</div>
|
|
<div class="meter">
|
|
<div id="storage-overlay-bar" class="bar"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card" id="active-url-card">
|
|
<div class="mini-title"><%:Active URL%></div>
|
|
<div id="active-url-list" class="active-url-list"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="text/javascript">//<![CDATA[
|
|
(function() {
|
|
try {
|
|
localStorage.setItem('luci-menu-category', 'basic');
|
|
console.log('Dashboard: Force set menu category to basic');
|
|
} catch(e) {
|
|
console.error('Dashboard: Failed to set menu category:', e);
|
|
}
|
|
})();
|
|
|
|
var _Unknown = '<%:Unknown%>';
|
|
var _OnlineApp = '<%:Online App%>';
|
|
var _OnlineUser = '<%:Online User%>';
|
|
var _User = '<%:User%>';
|
|
var _Connections = '<%:Connections%>';
|
|
var _FilterRules = '<%:Filter Rules%>';
|
|
var _TodayTraffic = '<%:Today Traffic%>';
|
|
var _RealtimeTraffic = '<%:Real-time Traffic%>';
|
|
var _Upload = '<%:Upload%>';
|
|
var _Download = '<%:Download%>';
|
|
var _ActiveApps = '<%:Active Apps%>';
|
|
var _AppUsageDistribution = '<%:App Usage Distribution%>';
|
|
var _LastHour = '<%:Last Hour%>';
|
|
var _Today = '<%:Today%>';
|
|
var _UserTrafficTop8 = '<%:User Traffic Top8%>';
|
|
var _ActiveTerminalsTop8 = '<%:Active Terminals Top8%>';
|
|
var _Name = '<%:Name%>';
|
|
var _IP = '<%:IP%>';
|
|
var _UpstreamRate = '<%:Upstream Rate%>';
|
|
var _DownstreamRate = '<%:Downstream Rate%>';
|
|
var _UpstreamTraffic = '<%:Upstream Traffic%>';
|
|
var _DownstreamTraffic = '<%:Downstream Traffic%>';
|
|
var _CurrentApp = '<%:Current App%>';
|
|
var _CurrentURL = '<%:Current URL%>';
|
|
var _SystemInfo = '<%:System Info%>';
|
|
var _OAFStatus = '<%:OAF Status%>';
|
|
var _Hostname = '<%:Hostname%>';
|
|
var _DeviceModel = '<%:Device Model%>';
|
|
var _CPU = 'CPU';
|
|
var _FirmwareVersion = '<%:Firmware Version%>';
|
|
var _KernelVersion = '<%:Kernel Version%>';
|
|
var _Uptime = '<%:Uptime%>';
|
|
var _RunningStatus = '<%:Running Status%>';
|
|
var _Memory = '<%:Memory%>';
|
|
var _Network = '<%:Network%>';
|
|
var _PortStatus = '<%:Port Status%>';
|
|
var _Gateway = '<%:Gateway%>';
|
|
var _GatewayMode = '<%:Gateway Mode%>';
|
|
var _BypassMode = '<%:Bypass Mode%>';
|
|
var _DNS = '<%:DNS%>';
|
|
var _Mask = '<%:Mask%>';
|
|
var _Temperature = '<%:Temperature%>';
|
|
var _TempSpace = '<%:Temp Space%>';
|
|
var _DiskSpace = '<%:Disk Space%>';
|
|
var _BootSpace = '<%:Boot Space%>';
|
|
var _ActiveURL = '<%:Active URL%>';
|
|
var _NoData = '<%:No Data%>';
|
|
var _Total = '<%:Total%>';
|
|
var _Day = '<%:Day%>';
|
|
var _Hour = '<%:Hour%>';
|
|
var _Minute = '<%:Minute%>';
|
|
var _Upstream = '<%:Upstream%>';
|
|
var _Downstream = '<%:Downstream%>';
|
|
var _Enabled = '<%:Enabled%>';
|
|
var _Disabled = '<%:Disabled%>';
|
|
var _Lite = '<%:Lite%>';
|
|
var _Plus = '<%:Plus%>';
|
|
var _Free = '<%:Free%>';
|
|
var _Premium = '<%:Premium%>';
|
|
var _AppID = '<%:App ID%>';
|
|
var _AccessTime = '<%:Access Time%>';
|
|
var _MACAddress = '<%:MAC Address%>';
|
|
var _Speed = '<%:Speed%>';
|
|
var _Status = '<%:Status%>';
|
|
var _Duplex = '<%:Duplex%>';
|
|
var _RxBytes = '<%:Rx Bytes%>';
|
|
var _TxBytes = '<%:Tx Bytes%>';
|
|
var _RxPackets = '<%:Rx Packets%>';
|
|
var _TxPackets = '<%:Tx Packets%>';
|
|
var _RxErrorPackets = '<%:Rx Error Packets%>';
|
|
var _TxErrorPackets = '<%:Tx Error Packets%>';
|
|
var _SourceIP = '<%:Source IP%>';
|
|
var _DestinationIP = '<%:Destination IP%>';
|
|
var _Protocol = '<%:Protocol%>';
|
|
var _SourcePort = '<%:Source Port%>';
|
|
var _DestinationPort = '<%:Destination Port%>';
|
|
var _MatchedDomain = '<%:Matched Domain%>';
|
|
var _IPAddress = '<%:IP Address%>';
|
|
var _Expanded = '<%:Expanded%>';
|
|
var _SwitchModeGuide = '<%:Tip: Click here to switch modes.%>';
|
|
var _SwitchModeGuideTitle = '<%:Quick Guide%>';
|
|
var _SwitchModeGuideAction = '<%:Got it%>';
|
|
var shouldShowModeSwitchGuide = false;
|
|
var modeSwitchGuideDismissed = false;
|
|
var initStatusFetched = false;
|
|
var initStatusSetting = false;
|
|
var modeSwitchGuideRetryCount = 0;
|
|
var MODE_SWITCH_GUIDE_MAX_RETRY = 20;
|
|
|
|
function pct(v){ return Math.max(0, Math.min(100, Math.round(v))); }
|
|
function clamp(v, min, max){ return Math.max(min, Math.min(max, v)); }
|
|
function getRootCssVar(name, defaultValue){
|
|
var value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
return value || defaultValue;
|
|
}
|
|
function isDarkMode(){
|
|
return document.documentElement.getAttribute('data-darkmode') === 'true';
|
|
}
|
|
|
|
function getTempBarGradient(tempC){
|
|
var t = clamp(parseFloat(tempC) || 0, 20, 100);
|
|
var ratio = (t - 20) / 80;
|
|
var startL = Math.round(84 - ratio * 6);
|
|
var endL = Math.round(62 - ratio * 14);
|
|
return 'linear-gradient(90deg,hsl(198,58%,' + startL + '%),hsl(32,100%,' + endL + '%))';
|
|
}
|
|
|
|
function setBar(id, txt, val){
|
|
var $bar = $('#' + id);
|
|
var p = pct(val);
|
|
$bar.css('width', p + "%");
|
|
if(txt && txt.length > 0) {
|
|
var $t = $('#' + txt);
|
|
$t.text(p + "%");
|
|
}
|
|
}
|
|
|
|
function setCircularProgress(ringId, textId, val){
|
|
var $ring = $('#' + ringId);
|
|
var $text = $('#' + textId);
|
|
var p = pct(val);
|
|
|
|
var radius = 35;
|
|
var circumference = 2 * Math.PI * radius;
|
|
|
|
var offset = circumference - (p / 100) * circumference;
|
|
$ring.css({
|
|
'stroke-dasharray': circumference,
|
|
'stroke-dashoffset': offset
|
|
});
|
|
|
|
$text.text(p + "%");
|
|
}
|
|
|
|
function formatMemory(memKB) {
|
|
var memMB = memKB / 1024;
|
|
if(memMB >= 1024) {
|
|
return (memMB / 1024).toFixed(2) + 'GB';
|
|
} else {
|
|
return Math.round(memMB) + 'MB';
|
|
}
|
|
}
|
|
|
|
function formatStorage(kb) {
|
|
var mb = kb / 1024;
|
|
if(mb >= 1024) {
|
|
return (mb / 1024).toFixed(2) + 'GB';
|
|
} else {
|
|
return Math.round(mb) + 'MB';
|
|
}
|
|
}
|
|
|
|
function setMemCircularProgress(ringId, textId, val, totalKB){
|
|
var $ring = $('#' + ringId);
|
|
var $text = $('#' + textId);
|
|
var p = pct(val);
|
|
|
|
var radius = 35;
|
|
var circumference = 2 * Math.PI * radius;
|
|
|
|
var offset = circumference - (p / 100) * circumference;
|
|
$ring.css({
|
|
'stroke-dasharray': circumference,
|
|
'stroke-dashoffset': offset
|
|
});
|
|
|
|
var usedKB = Math.round((p / 100) * totalKB);
|
|
$text.text(formatMemory(usedKB));
|
|
}
|
|
|
|
function randomWalk(cur, step){
|
|
var n = cur + (Math.random() - 0.5) * step;
|
|
return Math.max(0, n);
|
|
}
|
|
|
|
function fetchDashboardCommon(){
|
|
new XHR().get('<%=url("admin/services/oaf/api/dashboard/get_dashboard_common")%>', null, function(x, data){
|
|
if(data){
|
|
try {
|
|
var json = typeof data === 'string' ? JSON.parse(data) : data;
|
|
updateDashboardData(json);
|
|
} catch(e) {
|
|
console.error('Failed to parse dashboard data:', e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function dismissAccelerationWarning(){
|
|
$('#acceleration-warning').hide();
|
|
new XHR().post('<%=url("admin/services/oaf/api/dashboard/set_notice_status")%>', {}, function() {});
|
|
}
|
|
|
|
function bindTopStatNav(){
|
|
$('#app-count').closest('.mini').on('click', function(){
|
|
location.href = '<%=url("admin/services/oaf/app_record")%>';
|
|
});
|
|
$('#mini-online').closest('.mini').on('click', function(){
|
|
location.href = '<%=url("admin/services/oaf/users/list")%>';
|
|
});
|
|
$('#mini-rule-count').closest('.mini').on('click', function(){
|
|
var afCount = window._afRuleCount || 0;
|
|
var mfCount = window._mfRuleCount || 0;
|
|
if (afCount > 0 || mfCount === 0) {
|
|
location.href = '<%=url("admin/services/oaf/app_filter/rules")%>';
|
|
} else {
|
|
location.href = '<%=url("admin/services/oaf/mac_filter/rules")%>';
|
|
}
|
|
});
|
|
$('#oaf-feature-version').on('click', function(){
|
|
location.href = '<%=url("admin/services/oaf/feature")%>';
|
|
});
|
|
$('#oaf-app-record-count').on('click', function(){
|
|
location.href = '<%=url("admin/services/oaf/app_record")%>?tab=history';
|
|
});
|
|
$('.top-stats .mini').on('mousemove', function(e) {
|
|
var $tip = $(this).find('.tooltip');
|
|
if (!$tip.length) {
|
|
return;
|
|
}
|
|
var rect = this.getBoundingClientRect();
|
|
var tipW = 190;
|
|
var tipH = 90;
|
|
var x = e.clientX - rect.left + 12;
|
|
if (e.clientX + 12 + tipW > window.innerWidth - 8) {
|
|
x = e.clientX - rect.left - 12 - tipW;
|
|
}
|
|
var y = e.clientY - rect.top + 12;
|
|
if (e.clientY + 12 + tipH > window.innerHeight - 8) {
|
|
y = e.clientY - rect.top - 12 - tipH;
|
|
}
|
|
$tip.css({ left: x, top: y }).show();
|
|
}).on('mouseleave', function() {
|
|
$(this).find('.tooltip').hide();
|
|
});
|
|
}
|
|
|
|
function fetchInitStatusOnce(){
|
|
if (initStatusFetched) {
|
|
return;
|
|
}
|
|
initStatusFetched = true;
|
|
new XHR().get('<%=url("admin/services/oaf/api/dashboard/get_init_status")%>', null, function(x, data){
|
|
if(!data){
|
|
return;
|
|
}
|
|
try {
|
|
var json = typeof data === 'string' ? JSON.parse(data) : data;
|
|
shouldShowModeSwitchGuide = parseInt(json.init_status, 10) === 0;
|
|
updateModeSwitchGuide();
|
|
} catch(e) {
|
|
console.error('Failed to parse init status data:', e);
|
|
}
|
|
});
|
|
}
|
|
|
|
function fetchDashboardData(){
|
|
if(isRenderingHosts) {
|
|
return;
|
|
}
|
|
|
|
new XHR().get('<%=url("admin/services/oaf/api/dashboard/get_daily_top_users")%>', null, function(x, data){
|
|
if(data){
|
|
try {
|
|
var json = typeof data === 'string' ? JSON.parse(data) : data;
|
|
updateUserTrafficChart(json);
|
|
} catch(e) {
|
|
console.error('Failed to parse user traffic data:', e);
|
|
}
|
|
}
|
|
});
|
|
|
|
new XHR().get('<%=url("admin/services/oaf/api/dashboard/get_active_users")%>?count=8', null, function(x, data){
|
|
if(data){
|
|
try {
|
|
var json = typeof data === 'string' ? JSON.parse(data) : data;
|
|
if(json && json.users && Array.isArray(json.users)) {
|
|
activeUsersData = json.users;
|
|
cacheUsersByMac(activeUsersData);
|
|
renderClientList();
|
|
} else {
|
|
activeUsersData = [];
|
|
renderClientList();
|
|
}
|
|
} catch(e) {
|
|
console.error('Failed to parse active users data:', e);
|
|
activeUsersData = [];
|
|
renderClientList();
|
|
}
|
|
} else {
|
|
activeUsersData = [];
|
|
renderClientList();
|
|
}
|
|
});
|
|
}
|
|
|
|
function fetchHistoryTrafficData(type){
|
|
type = type || currentTrafficStatsType || 'today';
|
|
var url = type === 'days30' ?
|
|
'<%=url("admin/services/oaf/api/dashboard/get_history_traffic_stats")%>?days=30' :
|
|
'<%=url("admin/services/oaf/api/dashboard/get_global_traffic_stats")%>';
|
|
|
|
new XHR().get(url, null, function(x, data){
|
|
if(data){
|
|
try {
|
|
var json = typeof data === 'string' ? JSON.parse(data) : data;
|
|
updateHistoryTrafficChart(json, type);
|
|
} catch(e) {
|
|
console.error('Failed to parse traffic stats data:', e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function isEmptyValue(value) {
|
|
return value === undefined || value === null || value === '' || value === '0.0.0.0';
|
|
}
|
|
|
|
function formatEmptyValue(value, defaultValue) {
|
|
defaultValue = defaultValue || '--';
|
|
return isEmptyValue(value) ? defaultValue : value;
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value === undefined || value === null ? '' : value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function formatPortSpeed(speed) {
|
|
var value = parseInt(speed, 10) || 0;
|
|
var speedMap = {
|
|
10: '10Mbps',
|
|
100: '100Mbps',
|
|
1000: '1Gbps',
|
|
2500: '2.5Gbps',
|
|
5000: '5Gbps',
|
|
10000: '10Gbps'
|
|
};
|
|
if (speedMap[value]) {
|
|
return speedMap[value];
|
|
}
|
|
if (value > 1000) {
|
|
return (value / 1000).toFixed(value % 1000 === 0 ? 0 : 1) + 'Gbps';
|
|
}
|
|
return value > 0 ? value + 'Mbps' : '--';
|
|
}
|
|
|
|
function getPortSpeedClass(port) {
|
|
var speed = parseInt(port && port.speed, 10) || 0;
|
|
if (!port || parseInt(port.up, 10) !== 1) {
|
|
return 'port-down';
|
|
}
|
|
if (speed > 1000) return 'port-up port-speed-high';
|
|
if (speed === 1000) return 'port-up port-speed-mid';
|
|
return 'port-up port-speed-low';
|
|
}
|
|
|
|
function renderPortStatus(portList) {
|
|
var $grid = $('#port-status-grid');
|
|
if (!$grid.length) {
|
|
return;
|
|
}
|
|
if (!Array.isArray(portList) || portList.length === 0) {
|
|
$grid.html('<div class="port-empty" style="grid-column:1 / -1;">' + _NoData + '</div>');
|
|
return;
|
|
}
|
|
|
|
var html = '';
|
|
var sortedPorts = portList.slice().sort(function(a, b) {
|
|
var roleA = a && a.role === 'wan' ? 0 : 1;
|
|
var roleB = b && b.role === 'wan' ? 0 : 1;
|
|
if (roleA !== roleB) {
|
|
return roleA - roleB;
|
|
}
|
|
return 0;
|
|
});
|
|
sortedPorts.slice(0, 8).forEach(function(port) {
|
|
var name = port && port.name ? String(port.name) : '--';
|
|
var role = port && port.role ? String(port.role).toLowerCase() : '';
|
|
var displayName = role === 'wan' ? 'wan' : name;
|
|
var speedText = formatPortSpeed(port && port.speed);
|
|
var statusText = port && parseInt(port.up, 10) === 1 ? 'Up' : 'Down';
|
|
var duplexText = port && port.duplex && String(port.duplex).toLowerCase() !== 'unknown' ? String(port.duplex) : '--';
|
|
var duplexSuffix = '';
|
|
if (duplexText !== '--') {
|
|
var duplexLower = duplexText.toLowerCase();
|
|
if (duplexLower === 'full') {
|
|
duplexSuffix = '(全双工)';
|
|
} else if (duplexLower === 'half') {
|
|
duplexSuffix = '(半双工)';
|
|
} else {
|
|
duplexSuffix = '(' + duplexText + ')';
|
|
}
|
|
}
|
|
var speedDetailText = speedText + duplexSuffix;
|
|
var macText = port && port.mac ? String(port.mac) : '--';
|
|
var tooltipHtml = '<div class="tooltip-row"><span class="tooltip-label">' + _Name + ':</span><span class="tooltip-value">' + escapeHtml(displayName) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _Status + ':</span><span class="tooltip-value">' + escapeHtml(statusText) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _Speed + ':</span><span class="tooltip-value">' + escapeHtml(speedDetailText) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _MACAddress + ':</span><span class="tooltip-value">' + escapeHtml(macText) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _RxBytes + ':</span><span class="tooltip-value">' + escapeHtml(port && port.rx_bytes !== undefined ? port.rx_bytes : 0) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _TxBytes + ':</span><span class="tooltip-value">' + escapeHtml(port && port.tx_bytes !== undefined ? port.tx_bytes : 0) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _RxPackets + ':</span><span class="tooltip-value">' + escapeHtml(port && port.rx_packets !== undefined ? port.rx_packets : 0) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _TxPackets + ':</span><span class="tooltip-value">' + escapeHtml(port && port.tx_packets !== undefined ? port.tx_packets : 0) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _RxErrorPackets + ':</span><span class="tooltip-value">' + escapeHtml(port && port.rx_error_packets !== undefined ? port.rx_error_packets : 0) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _TxErrorPackets + ':</span><span class="tooltip-value">' + escapeHtml(port && port.tx_error_packets !== undefined ? port.tx_error_packets : 0) + '</span></div>';
|
|
html += '<div class="port-item ' + getPortSpeedClass(port) + '">' +
|
|
'<span class="port-icon"></span>' +
|
|
'<span class="port-name">' + escapeHtml(displayName) + '</span>' +
|
|
'<span class="port-speed">' + escapeHtml(speedText) + '</span>' +
|
|
'<span class="tooltip tooltip-left">' + tooltipHtml + '</span>' +
|
|
'</div>';
|
|
});
|
|
$grid.html(html);
|
|
}
|
|
|
|
function normalizeDnsValue(value) {
|
|
if (Array.isArray(value)) {
|
|
return value.filter(function(item) {
|
|
return !isEmptyValue(item);
|
|
}).join(' / ');
|
|
}
|
|
if (typeof value === 'string') {
|
|
return value;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function maskToPrefix(mask) {
|
|
if (isEmptyValue(mask)) {
|
|
return null;
|
|
}
|
|
var parts = String(mask).split('.');
|
|
if (parts.length !== 4) {
|
|
return null;
|
|
}
|
|
var bits = '';
|
|
for (var i = 0; i < parts.length; i++) {
|
|
var n = parseInt(parts[i], 10);
|
|
if (isNaN(n) || n < 0 || n > 255) {
|
|
return null;
|
|
}
|
|
var partBits = n.toString(2);
|
|
while (partBits.length < 8) {
|
|
partBits = '0' + partBits;
|
|
}
|
|
bits += partBits;
|
|
}
|
|
if (bits.indexOf('01') !== -1) {
|
|
return null;
|
|
}
|
|
return bits.replace(/0/g, '').length;
|
|
}
|
|
|
|
function formatIpWithPrefix(ip, mask) {
|
|
if (isEmptyValue(ip)) {
|
|
return '--';
|
|
}
|
|
var prefix = maskToPrefix(mask);
|
|
if (prefix === null) {
|
|
return ip;
|
|
}
|
|
return ip + '/' + prefix;
|
|
}
|
|
|
|
function setSummaryValue(id, value) {
|
|
var text = formatEmptyValue(value);
|
|
var $el = $('#' + id);
|
|
$el.text(text).attr('title', text);
|
|
}
|
|
|
|
function updateSystemTooltip(sys, versionStr, uptimeStr) {
|
|
var hostname = formatEmptyValue(sys.hostname);
|
|
var cpuModel = formatEmptyValue(sys.cpu_model_name);
|
|
var kernelVersion = formatEmptyValue(sys.kernel_version);
|
|
var model = formatEmptyValue(sys.model);
|
|
var tooltipVersionStr = versionStr;
|
|
if (tooltipVersionStr && tooltipVersionStr !== '--' && parseInt(sys.expand_root, 10) === 1) {
|
|
tooltipVersionStr += ' (' + _Expanded + ')';
|
|
}
|
|
var html = '<div class="tooltip-row"><span class="tooltip-label">' + _Hostname + ':</span><span class="tooltip-value">' + hostname + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _CPU + ':</span><span class="tooltip-value">' + cpuModel + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _KernelVersion + ':</span><span class="tooltip-value">' + kernelVersion + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _DeviceModel + ':</span><span class="tooltip-value">' + model + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _FirmwareVersion + ':</span><span class="tooltip-value">' + formatEmptyValue(tooltipVersionStr) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _Uptime + ':</span><span class="tooltip-value">' + formatEmptyValue(uptimeStr) + '</span></div>';
|
|
$('#sysinfo-tooltip').html(html);
|
|
}
|
|
|
|
function formatOpenwrtVersion(sys) {
|
|
if (sys.openwrt_version === 'SNAPSHOT' && sys.release_date) {
|
|
return 's' + sys.release_date;
|
|
}
|
|
return sys.openwrt_version;
|
|
}
|
|
|
|
function formatReleaseTypeSuffix(sys) {
|
|
var releaseType = parseInt(sys.release_type, 10);
|
|
if (releaseType === 1) {
|
|
return '(alpha)';
|
|
}
|
|
if (releaseType === 2) {
|
|
return '(beta)';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function formatFeatureLibraryVersion(version, type) {
|
|
var text = String(version || '').trim();
|
|
if (!text) {
|
|
return '--';
|
|
}
|
|
return 'v' + text.replace(/^[vV]/, '') + '(' + (Number(type) === 1 ? _Plus : _Lite) + ')';
|
|
}
|
|
|
|
function formatFeatureLibraryType(free) {
|
|
return Number(free) === 1 ? _Free : _Premium;
|
|
}
|
|
|
|
function formatWorkMode(mode) {
|
|
var value = parseInt(mode, 10);
|
|
if (value === 0) {
|
|
return _GatewayMode;
|
|
}
|
|
if (value === 1) {
|
|
return _BypassMode;
|
|
}
|
|
return '--';
|
|
}
|
|
|
|
function setCompactValue(id, value) {
|
|
var text = formatEmptyValue(value);
|
|
$('#' + id).text(text).attr('title', text);
|
|
}
|
|
|
|
function isX86Arch(arch) {
|
|
return arch === 'x86-64' || arch === 'x86_64';
|
|
}
|
|
|
|
function removeModeSwitchGuideBubble() {
|
|
$('#mode-switch-guide-bubble').remove();
|
|
$(window).off('resize.mode_guide scroll.mode_guide');
|
|
$(document).off('click.mode_guide_ack');
|
|
}
|
|
|
|
function acknowledgeInitStatus() {
|
|
if (initStatusSetting) {
|
|
return;
|
|
}
|
|
initStatusSetting = true;
|
|
new XHR().post('<%=url("admin/services/oaf/api/dashboard/set_init_status")%>', {}, function() {
|
|
initStatusSetting = false;
|
|
});
|
|
}
|
|
|
|
function positionModeSwitchGuideBubble() {
|
|
var $btn = $('.top-mode-menu .top-more-button');
|
|
var $bubble = $('#mode-switch-guide-bubble');
|
|
if (!$btn.length || !$bubble.length) {
|
|
return false;
|
|
}
|
|
var rect = $btn[0].getBoundingClientRect();
|
|
var bubbleWidth = $bubble.outerWidth() || 220;
|
|
var left = rect.left + rect.width - bubbleWidth;
|
|
if (left < 8) {
|
|
left = 8;
|
|
}
|
|
var top = rect.bottom + 10;
|
|
$bubble.css({
|
|
left: left + 'px',
|
|
top: top + 'px'
|
|
});
|
|
return true;
|
|
}
|
|
|
|
function updateModeSwitchGuide() {
|
|
if (!(shouldShowModeSwitchGuide && !modeSwitchGuideDismissed)) {
|
|
removeModeSwitchGuideBubble();
|
|
modeSwitchGuideRetryCount = 0;
|
|
return;
|
|
}
|
|
|
|
var $bubble = $('#mode-switch-guide-bubble');
|
|
if (!$bubble.length) {
|
|
$bubble = $('<div id="mode-switch-guide-bubble" class="mode-switch-guide-bubble">' +
|
|
'<div class="mode-switch-guide-title"></div>' +
|
|
'<p class="mode-switch-guide-text"></p>' +
|
|
'<div class="mode-switch-guide-actions"><button type="button" id="mode-switch-guide-ok" class="mode-switch-guide-ok"></button></div>' +
|
|
'</div>');
|
|
$bubble.find('.mode-switch-guide-title').text(_SwitchModeGuideTitle);
|
|
$bubble.find('.mode-switch-guide-text').text(_SwitchModeGuide);
|
|
$bubble.find('#mode-switch-guide-ok').text(_SwitchModeGuideAction);
|
|
$('body').append($bubble);
|
|
}
|
|
|
|
if (!positionModeSwitchGuideBubble()) {
|
|
if (modeSwitchGuideRetryCount < MODE_SWITCH_GUIDE_MAX_RETRY) {
|
|
modeSwitchGuideRetryCount++;
|
|
setTimeout(function() {
|
|
updateModeSwitchGuide();
|
|
}, 300);
|
|
}
|
|
return;
|
|
}
|
|
modeSwitchGuideRetryCount = 0;
|
|
|
|
$(window).off('resize.mode_guide scroll.mode_guide').on('resize.mode_guide scroll.mode_guide', function() {
|
|
positionModeSwitchGuideBubble();
|
|
});
|
|
|
|
$(document).off('click.mode_guide_ack').on('click.mode_guide_ack', '#mode-switch-guide-ok', function(e) {
|
|
e.preventDefault();
|
|
modeSwitchGuideDismissed = true;
|
|
shouldShowModeSwitchGuide = false;
|
|
acknowledgeInitStatus();
|
|
removeModeSwitchGuideBubble();
|
|
});
|
|
}
|
|
|
|
function updateDashboardData(data){
|
|
var sys = data.system_status || {};
|
|
var net = data.network_status || {};
|
|
var apps = data.active_app || {};
|
|
var hosts = data.active_host || {};
|
|
var traffic = data.interface_traffic || {};
|
|
var advanced = data.advanced || {};
|
|
var oaf = data.oaf_status || {};
|
|
var feature = oaf.feature || {};
|
|
$('#acceleration-warning').css('display', parseInt(advanced.notice_status, 10) === 1 ? 'none' : 'flex');
|
|
|
|
var versionStr = '';
|
|
var openwrtVersion = formatOpenwrtVersion(sys);
|
|
if(openwrtVersion && sys.fwx_version) {
|
|
versionStr = openwrtVersion + '-' + sys.fwx_version;
|
|
} else if(openwrtVersion) {
|
|
versionStr = openwrtVersion;
|
|
} else if(sys.fwx_version) {
|
|
versionStr = sys.fwx_version;
|
|
} else {
|
|
versionStr = '--';
|
|
}
|
|
if (versionStr !== '--') {
|
|
versionStr += formatReleaseTypeSuffix(sys);
|
|
}
|
|
|
|
var uptimeStr = '--';
|
|
if(sys.uptime) {
|
|
uptimeStr = formatUptime(sys.uptime);
|
|
}
|
|
|
|
setCompactValue('oaf-version', oaf.version || '--');
|
|
setCompactValue('oaf-engine-version', oaf.engine_version || '');
|
|
setCompactValue('oaf-work-mode', formatWorkMode(oaf.work_mode));
|
|
setCompactValue('oaf-feature-version', feature.version || '--');
|
|
setCompactValue('oaf-app-record-count', oaf.app_record_count === undefined ? 0 : oaf.app_record_count);
|
|
|
|
if(sys.total_mem !== undefined) {
|
|
$('#sys-mem-total').text(formatMemory(sys.total_mem));
|
|
}
|
|
|
|
if(sys.connections !== undefined) {
|
|
$('#sys-connections').text(sys.connections);
|
|
}
|
|
|
|
if(sys.client_num !== undefined) {
|
|
$('#sys-client-num').text(sys.client_num);
|
|
}
|
|
|
|
var onlineUserCount = parseInt(
|
|
sys.online_user_count !== undefined ? sys.online_user_count : sys.client_num,
|
|
10
|
|
) || 0;
|
|
$('#mini-online').text(onlineUserCount);
|
|
$('#user-online-count').text(onlineUserCount);
|
|
$('#user-offline-count').text(parseInt(sys.offline_user_count, 10) || 0);
|
|
|
|
var filterRuleCount = parseInt(sys.filter_rule_count, 10);
|
|
if (isNaN(filterRuleCount)) {
|
|
filterRuleCount = (parseInt(sys.af_rule_count, 10) || 0) + (parseInt(sys.mf_rule_count, 10) || 0);
|
|
}
|
|
$('#mini-rule-count').text(filterRuleCount);
|
|
window._afRuleCount = parseInt(sys.af_rule_count, 10) || 0;
|
|
window._mfRuleCount = parseInt(sys.mf_rule_count, 10) || 0;
|
|
$('#rule-af-count').text(window._afRuleCount);
|
|
$('#rule-mf-count').text(window._mfRuleCount);
|
|
|
|
var hasOverlayStorage = false;
|
|
var hasTmpStorage = false;
|
|
var tmpPercent = 0;
|
|
var tmpText = '--';
|
|
if(sys.storage) {
|
|
var storage = sys.storage;
|
|
if(storage.tmp && storage.tmp.total_kb !== undefined && storage.tmp.used_kb !== undefined) {
|
|
var tmpTotal = parseInt(storage.tmp.total_kb, 10) || 0;
|
|
var tmpUsed = parseInt(storage.tmp.used_kb, 10) || 0;
|
|
tmpPercent = tmpTotal > 0 ? Math.round((tmpUsed / tmpTotal) * 100) : 0;
|
|
tmpText = formatStorage(tmpUsed) + ' / ' + formatStorage(tmpTotal);
|
|
hasTmpStorage = true;
|
|
}
|
|
var overlay = storage.overlay || storage.root;
|
|
if(overlay && overlay.total_kb !== undefined && overlay.used_kb !== undefined) {
|
|
var overlayTotal = parseInt(overlay.total_kb, 10) || 0;
|
|
var overlayUsed = parseInt(overlay.used_kb, 10) || 0;
|
|
var overlayPercent = overlayTotal > 0 ? Math.round((overlayUsed / overlayTotal) * 100) : 0;
|
|
var overlayText = formatStorage(overlayUsed) + ' / ' + formatStorage(overlayTotal);
|
|
var overlayDisplayText = overlayText + ' (' + overlayPercent + '%)';
|
|
$('#storage-overlay-text').text(overlayDisplayText).attr('title', overlayDisplayText);
|
|
setBar('storage-overlay-bar', '', overlayPercent);
|
|
$('#storage-overlay-bar').css('min-width', overlayPercent > 0 ? '4px' : '0');
|
|
hasOverlayStorage = true;
|
|
}
|
|
}
|
|
if (!hasOverlayStorage) {
|
|
$('#storage-overlay-text').text('--').attr('title', '--');
|
|
setBar('storage-overlay-bar', '', 0);
|
|
$('#storage-overlay-bar').css('min-width', '0');
|
|
}
|
|
|
|
if(sys.flow) {
|
|
var todayUp = parseInt(sys.flow.today_up) || 0; // KB
|
|
var todayDown = parseInt(sys.flow.today_down) || 0; // KB
|
|
var totalFlowKB = todayUp + todayDown;
|
|
$('#mini-traffic').text(formatTraffic(totalFlowKB * 1024));
|
|
}
|
|
|
|
if(sys.cpu !== undefined){
|
|
var cpuVal = parseFloat(sys.cpu) || 0;
|
|
setCircularProgress('cpu-ring','cpu-text',cpuVal);
|
|
}
|
|
if(sys.total_mem !== undefined && sys.used_mem !== undefined){
|
|
var memTotal = parseInt(sys.total_mem) || 256;
|
|
var memUsed = parseInt(sys.used_mem) || 0;
|
|
var memPercent = memTotal > 0 ? (memUsed / memTotal * 100) : 0;
|
|
setMemCircularProgress('mem-ring','mem-text',memPercent, memTotal);
|
|
}
|
|
|
|
var $envCard = $('#env-storage-card');
|
|
var $primaryLabel = $('#env-primary-label');
|
|
var hasCpuTemp = sys.cpu_temp !== undefined && sys.cpu_temp > 0;
|
|
|
|
if ($envCard.length) {
|
|
if (hasCpuTemp) {
|
|
$primaryLabel.text(_Temperature);
|
|
var cpuTempVal = Math.round(sys.cpu_temp);
|
|
var cpuTempWidth = Math.min(100, Math.max(0, cpuTempVal));
|
|
var cpuTempText = cpuTempVal + '\u00B0C';
|
|
$('#cpu-temp-text').text(cpuTempText).attr('title', cpuTempText);
|
|
$('#cpu-temp-bar')
|
|
.css('background', getTempBarGradient(cpuTempVal))
|
|
.css('width', cpuTempWidth + '%')
|
|
.css('min-width', cpuTempWidth > 0 ? '4px' : '0');
|
|
} else if (hasTmpStorage) {
|
|
$primaryLabel.text(_TempSpace);
|
|
var tmpDisplayText = tmpText + ' (' + tmpPercent + '%)';
|
|
$('#cpu-temp-text').text(tmpDisplayText).attr('title', tmpDisplayText);
|
|
$('#cpu-temp-bar')
|
|
.css('background', getRootCssVar('--temp-space-bar-gradient', 'linear-gradient(90deg,#71def1,#87c6ff)'))
|
|
.css('width', tmpPercent + '%')
|
|
.css('min-width', tmpPercent > 0 ? '4px' : '0');
|
|
} else {
|
|
$primaryLabel.text(_TempSpace);
|
|
$('#cpu-temp-text').text('--').attr('title', '--');
|
|
$('#cpu-temp-bar')
|
|
.css('background', getRootCssVar('--temp-bar-default-gradient', 'linear-gradient(90deg,#f89501,#db4702)'))
|
|
.css('width', '0')
|
|
.css('min-width', '0');
|
|
}
|
|
}
|
|
|
|
var lanDns = normalizeDnsValue(net.lan && net.lan.dns);
|
|
var wanDns = normalizeDnsValue(net.wan && net.wan.dns);
|
|
|
|
var wanGateway = (net.wan && net.wan.gateway) ? net.wan.gateway : '';
|
|
var lanGateway = (net.lan && net.lan.gateway) ? net.lan.gateway : '';
|
|
|
|
var isGatewayMode = (net.wan && net.wan.ip && net.wan.ip !== '0.0.0.0' && net.wan.ip !== '');
|
|
|
|
var networkData = {
|
|
work_mode: (net.work_mode !== undefined && net.work_mode !== null) ? net.work_mode : 1,
|
|
isGatewayMode: isGatewayMode,
|
|
lan: {
|
|
ip: net.lan && net.lan.ip || '0.0.0.0',
|
|
mask: net.lan && net.lan.mask || '255.255.255.0',
|
|
ipv6: net.lan && net.lan.ipv6 || '',
|
|
gateway: lanGateway,
|
|
dns: lanDns
|
|
},
|
|
wan: {
|
|
ip: net.wan && net.wan.ip || '0.0.0.0',
|
|
mask: net.wan && net.wan.mask || '255.255.255.0',
|
|
ipv6: net.wan && net.wan.ipv6 || '',
|
|
gateway: wanGateway,
|
|
dns: wanDns
|
|
}
|
|
};
|
|
updateNetworkInfo(networkData);
|
|
renderPortStatus(net.port_status || []);
|
|
|
|
var appList = apps.list;
|
|
if(!Array.isArray(appList)) {
|
|
appList = [];
|
|
}
|
|
updateActiveApps(appList);
|
|
|
|
var onlineAppCount = parseInt(
|
|
apps.online_total !== undefined ? apps.online_total : apps.total,
|
|
10
|
|
) || 0;
|
|
$('#app-count').text(onlineAppCount);
|
|
$('#app-online-count').text(onlineAppCount);
|
|
$('#app-offline-count').text(parseInt(apps.offline_total, 10) || 0);
|
|
|
|
if(traffic.interface && traffic.traffic){
|
|
updateInterfaceTraffic(traffic);
|
|
}
|
|
|
|
updateActiveHosts((hosts.list || []).slice(0, 8));
|
|
}
|
|
|
|
function formatUptime(seconds){
|
|
var days = Math.floor(seconds / 86400);
|
|
var hours = Math.floor((seconds % 86400) / 3600);
|
|
var minutes = Math.floor((seconds % 3600) / 60);
|
|
var secs = seconds % 60;
|
|
var result = [];
|
|
if(days > 0) result.push(days + 'd');
|
|
if(hours > 0) result.push(hours + 'h');
|
|
if(minutes > 0) result.push(minutes + 'm');
|
|
result.push(secs + 's');
|
|
return result.length > 0 ? result.join(' ') : '0s';
|
|
}
|
|
|
|
var cpu=18, mem=42;
|
|
var memTotalMB = 512;
|
|
|
|
setCircularProgress('cpu-ring','cpu-text',cpu);
|
|
setMemCircularProgress('mem-ring','mem-text',mem, memTotalMB);
|
|
|
|
setBar('storage-overlay-bar','',0);
|
|
|
|
var TRAFFIC_SAMPLE_POINTS = 60;
|
|
var FWX_ECHART_RENDERER = 'svg';
|
|
var pieApp = echarts.init($('#pie-app')[0], null, { renderer: FWX_ECHART_RENDERER });
|
|
var lineWanEl = $('#line-wan')[0];
|
|
var lineWan = lineWanEl ? echarts.init(lineWanEl, null, { renderer: FWX_ECHART_RENDERER }) : null;
|
|
var historyTraffic = echarts.init($('#history-traffic-chart')[0], null, { renderer: FWX_ECHART_RENDERER });
|
|
var barUserTraffic = echarts.init($('#bar-user-traffic')[0], null, { renderer: FWX_ECHART_RENDERER });
|
|
|
|
function resizeDashboardCharts() {
|
|
[pieApp, lineWan, historyTraffic, barUserTraffic].forEach(function(chart) {
|
|
if (chart) {
|
|
chart.resize();
|
|
}
|
|
});
|
|
}
|
|
|
|
var resizeDashboardChartsTimer = null;
|
|
$(window).on('resize.dashboardCharts', function() {
|
|
clearTimeout(resizeDashboardChartsTimer);
|
|
resizeDashboardChartsTimer = setTimeout(resizeDashboardCharts, 120);
|
|
});
|
|
if (window.ResizeObserver) {
|
|
var dashboardResizeObserver = new ResizeObserver(function() {
|
|
clearTimeout(resizeDashboardChartsTimer);
|
|
resizeDashboardChartsTimer = setTimeout(resizeDashboardCharts, 120);
|
|
});
|
|
var dashEl = document.querySelector('.dash');
|
|
if (dashEl) {
|
|
dashboardResizeObserver.observe(dashEl);
|
|
}
|
|
}
|
|
setTimeout(resizeDashboardCharts, 0);
|
|
setTimeout(resizeDashboardCharts, 300);
|
|
|
|
|
|
function getCSSVariable(name, defaultValue) {
|
|
var root = document.documentElement;
|
|
var value = getComputedStyle(root).getPropertyValue(name).trim();
|
|
return value || defaultValue;
|
|
}
|
|
|
|
var darkText = getCSSVariable('--chart-text-color', '#333333');
|
|
var axisLineColor = getCSSVariable('--chart-axis-line-color', 'rgba(0,0,0,0.2)');
|
|
var axisLabelColor = getCSSVariable('--chart-axis-label-color', '#666666');
|
|
var splitLineColor = getCSSVariable('--chart-split-line-color', 'rgba(0,0,0,0.08)');
|
|
var splitLineDashedColor = getCSSVariable('--chart-split-line-dashed-color', 'rgba(0,0,0,0.15)');
|
|
var tooltipBg = getCSSVariable('--chart-tooltip-bg', 'rgba(255,255,255,0.95)');
|
|
var tooltipText = getCSSVariable('--chart-tooltip-text', '#333333');
|
|
|
|
var axis = {
|
|
lineStyle:{color:axisLineColor},
|
|
axisLabel:{color:axisLabelColor},
|
|
axisLine:{lineStyle:{color:axisLineColor}},
|
|
splitLine:{lineStyle:{color:splitLineColor}}
|
|
};
|
|
var palette = [
|
|
'#58a6ff',
|
|
'#8b5cf6',
|
|
'#72b7a1',
|
|
'#4dc7ff',
|
|
'#f6b26b',
|
|
'#14b8a6',
|
|
'#d39f6a',
|
|
'#22c55e',
|
|
'#f59e0b',
|
|
'#c79abb',
|
|
'#fda4af',
|
|
'#14b8a6'
|
|
];
|
|
var userTrafficUpColor = '#58a6ff';
|
|
var userTrafficDownColor = '#8b7bff';
|
|
|
|
var currentAppStatsType = 'daily';
|
|
var currentTrafficStatsType = 'today';
|
|
|
|
var appStatsData = {
|
|
hourly: null,
|
|
daily: null
|
|
};
|
|
|
|
pieApp.setOption({
|
|
backgroundColor: 'transparent',
|
|
color: palette,
|
|
textStyle:{ color: axisLabelColor },
|
|
tooltip:{
|
|
trigger:'item',
|
|
backgroundColor: tooltipBg,
|
|
textStyle: { color: tooltipText },
|
|
formatter: function(params) {
|
|
var name = params.name || _Unknown;
|
|
var value = params.value || 0;
|
|
var percent = params.percent || 0;
|
|
var hours = Math.floor(value / 3600);
|
|
var minutes = Math.floor((value % 3600) / 60);
|
|
var timeStr = '';
|
|
if (hours > 0) {
|
|
timeStr = hours + _Hour + (minutes > 0 ? minutes + _Minute : '');
|
|
} else {
|
|
timeStr = minutes + _Minute;
|
|
}
|
|
return name + '<br/>' + timeStr + ' (' + percent.toFixed(1) + '%)';
|
|
}
|
|
},
|
|
legend:{bottom:0, textStyle:{color:axisLabelColor}},
|
|
grid: {
|
|
top: '5%',
|
|
bottom: '10%'
|
|
},
|
|
series:[{
|
|
type:'pie',
|
|
radius:['50%','65%'],
|
|
center: ['50%', '45%'],
|
|
data:[]
|
|
}]
|
|
});
|
|
|
|
function updateAppTypeStatsChart(data) {
|
|
if (!data) {
|
|
pieApp.setOption({
|
|
series: [{
|
|
data: []
|
|
}]
|
|
});
|
|
return;
|
|
}
|
|
|
|
var typesArray = [];
|
|
if (Array.isArray(data.types)) {
|
|
typesArray = data.types;
|
|
} else if (data.types && typeof data.types === 'object') {
|
|
typesArray = Object.keys(data.types).map(function(key) {
|
|
return data.types[key];
|
|
});
|
|
}
|
|
|
|
var chartData = typesArray.map(function(item) {
|
|
return {
|
|
name: item.name || ('Category ' + item.app_type),
|
|
value: item.total_time || 0
|
|
};
|
|
});
|
|
|
|
pieApp.setOption({
|
|
series: [{
|
|
data: chartData
|
|
}]
|
|
});
|
|
resizeDashboardCharts();
|
|
}
|
|
|
|
|
|
function fetchAppTypeStats(type) {
|
|
type = type || 'hourly';
|
|
|
|
|
|
if (appStatsData[type]) {
|
|
updateAppTypeStatsChart(appStatsData[type]);
|
|
return;
|
|
}
|
|
|
|
var url = '<%=url("admin/services/oaf/api/dashboard/get_app_type_stats")%>?type=' + type;
|
|
new XHR().get(url, null, function(x, data) {
|
|
if (data) {
|
|
try {
|
|
var json = typeof data === 'string' ? JSON.parse(data) : data;
|
|
appStatsData[type] = json;
|
|
if (currentAppStatsType === type) {
|
|
updateAppTypeStatsChart(json);
|
|
}
|
|
} catch(e) {
|
|
console.error('Failed to parse app type stats data:', e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function fetchAllAppTypeStats() {
|
|
['hourly', 'daily'].forEach(function(type) {
|
|
var url = '<%=url("admin/services/oaf/api/dashboard/get_app_type_stats")%>?type=' + type;
|
|
new XHR().get(url, null, function(x, data) {
|
|
if (data) {
|
|
try {
|
|
var json = typeof data === 'string' ? JSON.parse(data) : data;
|
|
appStatsData[type] = json;
|
|
if (currentAppStatsType === type) {
|
|
updateAppTypeStatsChart(json);
|
|
}
|
|
} catch(e) {
|
|
console.error('Failed to parse app type stats data:', e);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
$('#app-stats-select').on('change', function() {
|
|
var type = $(this).val() || 'daily';
|
|
currentAppStatsType = type;
|
|
if (appStatsData[type]) {
|
|
updateAppTypeStatsChart(appStatsData[type]);
|
|
} else {
|
|
fetchAppTypeStats(type);
|
|
}
|
|
});
|
|
|
|
$('#traffic-stats-select').on('change', function() {
|
|
currentTrafficStatsType = $(this).val() || 'today';
|
|
fetchHistoryTrafficData(currentTrafficStatsType);
|
|
});
|
|
|
|
function initAppStatsCustomSwitch() {
|
|
var $native = $('#app-stats-select');
|
|
if (!$native.length || $('#app-stats-switch').length) {
|
|
return;
|
|
}
|
|
var appStatsLabelHourly = '<%:Last Hour%>';
|
|
var appStatsLabelDaily = '<%:Today%>';
|
|
var $switch = $('<span id="app-stats-switch" class="app-stats-switch">' +
|
|
'<button type="button" id="app-stats-switch-btn" class="app-stats-switch-btn">' + appStatsLabelDaily + '</button>' +
|
|
'<div id="app-stats-switch-menu" class="app-stats-switch-menu">' +
|
|
'<button type="button" class="app-stats-switch-item" data-type="hourly">' + appStatsLabelHourly + '</button>' +
|
|
'<button type="button" class="app-stats-switch-item active" data-type="daily">' + appStatsLabelDaily + '</button>' +
|
|
'</div>' +
|
|
'</span>');
|
|
|
|
$native.closest('.app-stats-select-wrap').after($switch);
|
|
|
|
var updateSwitch = function(type) {
|
|
var label = type === 'hourly' ? appStatsLabelHourly : appStatsLabelDaily;
|
|
$('#app-stats-switch-btn').text(label);
|
|
$('#app-stats-switch-menu .app-stats-switch-item').removeClass('active');
|
|
$('#app-stats-switch-menu .app-stats-switch-item[data-type="' + type + '"]').addClass('active');
|
|
};
|
|
updateSwitch(currentAppStatsType || 'daily');
|
|
|
|
$(document).on('click', '#app-stats-switch-btn', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
$('#app-stats-switch').toggleClass('open');
|
|
});
|
|
$(document).on('click', '#app-stats-switch-menu .app-stats-switch-item', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var type = $(this).data('type') || 'daily';
|
|
updateSwitch(type);
|
|
$('#app-stats-switch').removeClass('open');
|
|
$native.val(type).trigger('change');
|
|
});
|
|
$(document).on('click', function() {
|
|
$('#app-stats-switch').removeClass('open');
|
|
});
|
|
}
|
|
|
|
function initTrafficStatsCustomSwitch() {
|
|
var $native = $('#traffic-stats-select');
|
|
if (!$native.length || $('#traffic-stats-switch').length) {
|
|
return;
|
|
}
|
|
|
|
var todayText = $native.find('option[value="today"]').text() || 'Today';
|
|
var days30Text = $native.find('option[value="days30"]').text() || 'Last 30 Days';
|
|
var $switch = $('<span id="traffic-stats-switch" class="app-stats-switch">' +
|
|
'<button type="button" id="traffic-stats-switch-btn" class="app-stats-switch-btn"></button>' +
|
|
'<div id="traffic-stats-switch-menu" class="app-stats-switch-menu">' +
|
|
'<button type="button" class="app-stats-switch-item active" data-type="today"></button>' +
|
|
'<button type="button" class="app-stats-switch-item" data-type="days30"></button>' +
|
|
'</div>' +
|
|
'</span>');
|
|
|
|
$switch.find('[data-type="today"]').text(todayText);
|
|
$switch.find('[data-type="days30"]').text(days30Text);
|
|
$native.closest('.app-stats-select-wrap').after($switch);
|
|
|
|
var updateSwitch = function(type) {
|
|
var label = type === 'days30' ? days30Text : todayText;
|
|
$('#traffic-stats-switch-btn').text(label);
|
|
$('#traffic-stats-switch-menu .app-stats-switch-item').removeClass('active');
|
|
$('#traffic-stats-switch-menu .app-stats-switch-item[data-type="' + type + '"]').addClass('active');
|
|
};
|
|
updateSwitch(currentTrafficStatsType || 'today');
|
|
|
|
$(document).on('click', '#traffic-stats-switch-btn', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
$('#traffic-stats-switch').toggleClass('open');
|
|
});
|
|
$(document).on('click', '#traffic-stats-switch-menu .app-stats-switch-item', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var type = $(this).data('type') || 'today';
|
|
updateSwitch(type);
|
|
$('#traffic-stats-switch').removeClass('open');
|
|
$native.val(type).trigger('change');
|
|
});
|
|
$(document).on('click', function() {
|
|
$('#traffic-stats-switch').removeClass('open');
|
|
});
|
|
}
|
|
|
|
$(document).on('click', '#acceleration-warning-close', function(e) {
|
|
e.preventDefault();
|
|
dismissAccelerationWarning();
|
|
});
|
|
|
|
|
|
fetchDashboardCommon();
|
|
fetchDashboardData();
|
|
fetchHistoryTrafficData(currentTrafficStatsType);
|
|
fetchAllAppTypeStats();
|
|
initAppStatsCustomSwitch();
|
|
initTrafficStatsCustomSwitch();
|
|
bindTopStatNav();
|
|
|
|
setInterval(function(){
|
|
fetchDashboardCommon();
|
|
}, 3000);
|
|
|
|
setTimeout(function(){
|
|
setInterval(function(){
|
|
fetchDashboardData();
|
|
fetchAllAppTypeStats();
|
|
if (currentTrafficStatsType === 'today') {
|
|
fetchHistoryTrafficData(currentTrafficStatsType);
|
|
}
|
|
}, 3000);
|
|
}, 1500);
|
|
|
|
barUserTraffic.setOption({
|
|
backgroundColor: 'transparent',
|
|
color: [userTrafficUpColor, userTrafficDownColor],
|
|
textStyle: { color: axisLabelColor },
|
|
tooltip: {
|
|
trigger: 'item',
|
|
axisPointer: { type: 'shadow' },
|
|
backgroundColor: tooltipBg,
|
|
textStyle: { color: tooltipText },
|
|
formatter: function(params) {
|
|
if (params.seriesName !== _Downstream) {
|
|
return '';
|
|
}
|
|
var user = params.data.user;
|
|
if (!user) {
|
|
return '';
|
|
}
|
|
var nameInfo = formatUserDisplayName(user);
|
|
var displayName = nameInfo.display;
|
|
var fullName = nameInfo.full;
|
|
var upStr = formatTraffic(user.up_bytes);
|
|
var downStr = formatTraffic(user.down_bytes);
|
|
var totalStr = formatTraffic(user.total_bytes);
|
|
return displayName + '<br/>' +
|
|
'MAC: ' + (user.mac || '') + '<br/>' +
|
|
_IP + ': ' + user.ip + '<br/>' +
|
|
'<span style="color:' + userTrafficUpColor + ';">•</span> ' + _Upstream + ': ' + upStr + '<br/>' +
|
|
'<span style="color:' + userTrafficDownColor + ';">•</span> ' + _Downstream + ': ' + downStr + '<br/>' +
|
|
_Total + ': ' + totalStr;
|
|
},
|
|
confine: true
|
|
},
|
|
legend: {
|
|
show: true,
|
|
bottom: 0,
|
|
textStyle: { color: axisLabelColor },
|
|
data: [_Upstream, _Downstream]
|
|
},
|
|
grid: {
|
|
left: 8,
|
|
right: 8,
|
|
top: '2%',
|
|
bottom: '12%',
|
|
containLabel: true
|
|
},
|
|
xAxis: {
|
|
type: 'value',
|
|
position: 'bottom',
|
|
min: 0,
|
|
axisLabel: {
|
|
color: axisLabelColor,
|
|
fontSize: 10,
|
|
formatter: function(value) {
|
|
return formatTraffic(value);
|
|
}
|
|
},
|
|
axisLine: {
|
|
show: true,
|
|
lineStyle: { color: axisLineColor }
|
|
},
|
|
splitLine: {
|
|
show: false
|
|
},
|
|
splitNumber: 6
|
|
},
|
|
yAxis: [
|
|
{
|
|
type: 'category',
|
|
position: 'left',
|
|
boundaryGap: false,
|
|
axisLabel: {
|
|
color: axisLabelColor,
|
|
fontSize: 12,
|
|
fontWeight: 'bold',
|
|
formatter: function(value, index) {
|
|
return (index + 1).toString();
|
|
}
|
|
},
|
|
axisLine: {
|
|
show: true,
|
|
lineStyle: { color: axisLineColor }
|
|
},
|
|
splitLine: {
|
|
show: false
|
|
},
|
|
data: []
|
|
},
|
|
{
|
|
type: 'category',
|
|
position: 'right',
|
|
axisLabel: {
|
|
show: false
|
|
},
|
|
axisLine: {
|
|
show: false
|
|
},
|
|
splitLine: {
|
|
show: false
|
|
},
|
|
data: []
|
|
}
|
|
],
|
|
series: [
|
|
{
|
|
name: _Upstream,
|
|
type: 'bar',
|
|
stack: 'traffic',
|
|
data: [],
|
|
barWidth: 25,
|
|
barCategoryGap: '50%',
|
|
label: {
|
|
show: false
|
|
}
|
|
},
|
|
{
|
|
name: _Downstream,
|
|
type: 'bar',
|
|
stack: 'traffic',
|
|
data: [],
|
|
barWidth: 25,
|
|
barCategoryGap: '50%',
|
|
label: {
|
|
show: true,
|
|
position: 'right',
|
|
color: axisLabelColor,
|
|
fontSize: 11,
|
|
formatter: function(params) {
|
|
if (params.seriesName === _Downstream) {
|
|
if (params.data.user) {
|
|
var nameInfo = formatUserDisplayName(params.data.user);
|
|
var displayName = nameInfo.display;
|
|
var totalBytes = (params.data.user.up_bytes || 0) + (params.data.user.down_bytes || 0);
|
|
var totalTraffic = formatTraffic(totalBytes);
|
|
return displayName + '(' + totalTraffic + ')';
|
|
}
|
|
}
|
|
return '';
|
|
},
|
|
distance: 3,
|
|
overflow: 'truncate',
|
|
width: 130
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
var clientData = [];
|
|
|
|
var networkData = {
|
|
work_mode: 1,
|
|
lan: {ip: '', mask: '', ipv6: '', dns: '', gateway: ''},
|
|
wan: {ip: '', mask: '', ipv6: '', dns: '', gateway: ''}
|
|
};
|
|
|
|
function updateNetworkInfo(data){
|
|
if(data){
|
|
networkData = data;
|
|
}
|
|
renderNetworkInfo();
|
|
}
|
|
|
|
function renderNetworkInfo(){
|
|
var lan = networkData.lan || {};
|
|
var wan = networkData.wan || {};
|
|
var isGatewayMode = networkData.isGatewayMode !== undefined ? networkData.isGatewayMode : (wan && wan.ip && wan.ip !== '0.0.0.0' && wan.ip !== '');
|
|
|
|
var lanSummary = formatIpWithPrefix(lan.ip, lan.mask);
|
|
var wanSummary = isGatewayMode ? formatIpWithPrefix(wan.ip, wan.mask) : '--';
|
|
var lanSummaryDisplay = lanSummary;
|
|
var wanSummaryDisplay = wanSummary;
|
|
var gatewaySummary = isGatewayMode ? wan.gateway : lan.gateway;
|
|
var dnsSummary = isGatewayMode ? wan.dns : lan.dns;
|
|
|
|
setSummaryValue('network-lan-summary', lanSummaryDisplay);
|
|
setSummaryValue('network-wan-summary', wanSummaryDisplay);
|
|
setSummaryValue('network-gateway-summary', gatewaySummary);
|
|
setSummaryValue('network-dns-summary', dnsSummary);
|
|
|
|
updateNetworkTooltip();
|
|
}
|
|
|
|
function updateNetworkTooltip(){
|
|
var $tooltip = $('#network-tooltip');
|
|
var lan = networkData.lan || {};
|
|
var wan = networkData.wan || {};
|
|
|
|
var isGatewayMode = networkData.isGatewayMode !== undefined ? networkData.isGatewayMode : (wan && wan.ip && wan.ip !== '0.0.0.0' && wan.ip !== '');
|
|
|
|
var html = '<div class="tooltip-row"><span class="tooltip-label" style="font-weight:600;">LAN:</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _IPAddress + ':</span><span class="tooltip-value">' + formatEmptyValue(lan.ip) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _Mask + ':</span><span class="tooltip-value">' + formatEmptyValue(lan.mask) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">IPv6:</span><span class="tooltip-value">' + formatEmptyValue(lan.ipv6) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _Gateway + ':</span><span class="tooltip-value">' + formatEmptyValue(lan.gateway) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _DNS + ':</span><span class="tooltip-value">' + formatEmptyValue(lan.dns) + '</span></div>' +
|
|
'<div class="tooltip-row" style="margin-top:8px;"><span class="tooltip-label" style="font-weight:600;">WAN:</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _IPAddress + ':</span><span class="tooltip-value">' + formatEmptyValue(wan.ip) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _Mask + ':</span><span class="tooltip-value">' + formatEmptyValue(wan.mask) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">IPv6:</span><span class="tooltip-value">' + formatEmptyValue(wan.ipv6) + '</span></div>';
|
|
if (isGatewayMode) {
|
|
html += '<div class="tooltip-row"><span class="tooltip-label">' + _Gateway + ':</span><span class="tooltip-value">' + formatEmptyValue(wan.gateway) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _DNS + ':</span><span class="tooltip-value">' + formatEmptyValue(wan.dns) + '</span></div>';
|
|
} else {
|
|
html += '<div class="tooltip-row"><span class="tooltip-label">' + _Gateway + ':</span><span class="tooltip-value">' + formatEmptyValue(lan.gateway) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _DNS + ':</span><span class="tooltip-value">' + formatEmptyValue(lan.dns) + '</span></div>';
|
|
}
|
|
$tooltip.html(html);
|
|
}
|
|
|
|
function bindTooltipHover(titleId, tooltipId) {
|
|
var $title = $('#' + titleId);
|
|
if ($title.length) {
|
|
$title.on('mouseenter', function() {
|
|
$('#' + tooltipId).show();
|
|
}).on('mouseleave', function() {
|
|
$('#' + tooltipId).hide();
|
|
});
|
|
}
|
|
}
|
|
|
|
var currentInterface = '';
|
|
|
|
function calculateYAxisInterval(max) {
|
|
var KB = 1024;
|
|
var MB = 1024 * 1024;
|
|
var unit = KB;
|
|
if (max >= MB) {
|
|
unit = MB;
|
|
} else if (max >= KB) {
|
|
unit = KB;
|
|
} else {
|
|
unit = 1;
|
|
}
|
|
var smallSteps = [1, 2, 5, 10];
|
|
var tensSteps = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000];
|
|
var steps = max >= 10 * unit ? tensSteps : smallSteps;
|
|
var selected = steps[0] * unit;
|
|
for (var i = 0; i < steps.length; i++) {
|
|
var interval = steps[i] * unit;
|
|
if (interval * 6 >= max) {
|
|
selected = interval;
|
|
break;
|
|
}
|
|
}
|
|
if (selected * 6 < max) {
|
|
selected = steps[steps.length - 1] * unit;
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
function loadTrafficData(){
|
|
var newInterface = 'wan';
|
|
if(newInterface !== currentInterface){
|
|
currentInterface = newInterface;
|
|
updateTrafficTitle(currentInterface);
|
|
|
|
if(up.length === 0){
|
|
up = [];
|
|
down = [];
|
|
curUp = 2e7;
|
|
curDown = 5e7;
|
|
for(var i=0; i<TRAFFIC_SAMPLE_POINTS; i++){
|
|
up.push(0);
|
|
down.push(0);
|
|
}
|
|
}
|
|
}
|
|
curUp = randomWalk(curUp, 8e6);
|
|
curDown = randomWalk(curDown, 12e6);
|
|
up.push(curUp);
|
|
down.push(curDown);
|
|
up.shift();
|
|
down.shift();
|
|
|
|
var maxValue = Math.max.apply(Math, up.concat(down));
|
|
|
|
var interval = calculateYAxisInterval(maxValue);
|
|
|
|
var suggestedMax = interval * 6;
|
|
|
|
if (lineWan) {
|
|
lineWan.setOption({
|
|
yAxis: {
|
|
interval: interval,
|
|
max: suggestedMax
|
|
},
|
|
series:[{data:up},{data:down}]
|
|
});
|
|
}
|
|
|
|
updateWanRealtimeRate(curUp, curDown);
|
|
}
|
|
|
|
function updateTrafficTitle(interfaceName){
|
|
var $titleEl = $('#traffic-title');
|
|
if($titleEl.length){
|
|
if(interfaceName){
|
|
$titleEl.text(_RealtimeTraffic + ' (' + interfaceName + ')');
|
|
} else {
|
|
$titleEl.text(_RealtimeTraffic);
|
|
}
|
|
}
|
|
}
|
|
|
|
function bytes(v){
|
|
return formatRate(v);
|
|
}
|
|
|
|
function updateWanRealtimeRate(upBytes, downBytes) {
|
|
var $upRate = $('#wan-up-rate');
|
|
var $downRate = $('#wan-down-rate');
|
|
if ($upRate.length) {
|
|
$upRate.text(formatRate(upBytes));
|
|
}
|
|
if ($downRate.length) {
|
|
$downRate.text(formatRate(downBytes));
|
|
}
|
|
}
|
|
var x = [], up = [], down = [], curUp = 2e7, curDown = 5e7; // bytes/s
|
|
for (var i=0;i<TRAFFIC_SAMPLE_POINTS;i++){ x.push(i - (TRAFFIC_SAMPLE_POINTS - 1)); }
|
|
if (lineWan) {
|
|
lineWan.setOption({
|
|
backgroundColor: 'transparent',
|
|
color: ['#58a6ff', '#8b7bff'],
|
|
textStyle:{ color: axisLabelColor },
|
|
animation: false,
|
|
animationDurationUpdate: 0,
|
|
tooltip:{
|
|
trigger:'axis',
|
|
backgroundColor: tooltipBg,
|
|
textStyle: { color: tooltipText },
|
|
formatter:function(p){
|
|
return _Upstream + ': '+bytes(p[0].data)+'<br/>'+_Downstream + ': '+bytes(p[1].data);
|
|
}
|
|
},
|
|
grid: {
|
|
left: '8%',
|
|
right: '3%',
|
|
top: '15%',
|
|
bottom: '3%',
|
|
containLabel: false
|
|
},
|
|
xAxis:{
|
|
type:'category',
|
|
data:x,
|
|
boundaryGap:false,
|
|
axisLabel:{show:false, color:axisLabelColor},
|
|
axisLine:axis.axisLine,
|
|
splitLine:axis.splitLine
|
|
},
|
|
yAxis:{
|
|
type:'value',
|
|
min: 0,
|
|
interval: 20 * 1024,
|
|
max: 20 * 1024 * 6,
|
|
axisLabel:{
|
|
formatter:function(v){
|
|
return formatRate(v);
|
|
},
|
|
color:axisLabelColor
|
|
},
|
|
axisLine:axis.axisLine,
|
|
splitLine: {
|
|
show: true,
|
|
lineStyle: {
|
|
type: 'dashed',
|
|
color: splitLineDashedColor,
|
|
width: 1
|
|
}
|
|
}
|
|
},
|
|
series:[
|
|
{
|
|
name:_Upstream,
|
|
type:'line',
|
|
smooth:true,
|
|
symbol:'none',
|
|
lineStyle: {
|
|
width: 1.2
|
|
},
|
|
areaStyle:{opacity:0.18},
|
|
data:up
|
|
},
|
|
{
|
|
name:_Downstream,
|
|
type:'line',
|
|
smooth:true,
|
|
symbol:'none',
|
|
lineStyle: {
|
|
width: 1.2
|
|
},
|
|
areaStyle:{opacity:0.18},
|
|
data:down
|
|
}
|
|
],
|
|
legend:{ top:0, textStyle:{color:axisLabelColor} }
|
|
});
|
|
}
|
|
|
|
if(typeof up === 'undefined' || up.length === 0){
|
|
up = [];
|
|
down = [];
|
|
for(var i=0; i<TRAFFIC_SAMPLE_POINTS; i++){
|
|
up.push(0);
|
|
down.push(0);
|
|
}
|
|
}
|
|
|
|
var $activeTerminalList = $('#active-terminal-list');
|
|
var activeUsersData = [];
|
|
var activeUsersByMac = {};
|
|
|
|
function floorFixed(value, digits) {
|
|
var scale = Math.pow(10, digits);
|
|
return (Math.floor(value * scale) / scale).toFixed(digits);
|
|
}
|
|
|
|
function formatRate(bytesPerSec) {
|
|
var value = Number(bytesPerSec) || 0;
|
|
if (value >= 1024 * 1024) {
|
|
return Math.floor(value / (1024 * 1024)) + ' MB/s';
|
|
}
|
|
if (value >= 1024) {
|
|
return Math.floor(value / 1024) + ' KB/s';
|
|
}
|
|
return Math.floor(value) + ' B/s';
|
|
}
|
|
|
|
function formatTraffic(bytes) {
|
|
var value = Number(bytes) || 0;
|
|
var val;
|
|
var unit;
|
|
if (value >= 1024 * 1024 * 1024) {
|
|
val = value / (1024 * 1024 * 1024);
|
|
unit = ' GB';
|
|
} else if (value >= 1024 * 1024) {
|
|
val = value / (1024 * 1024);
|
|
unit = ' MB';
|
|
} else if (value >= 1024) {
|
|
val = value / 1024;
|
|
unit = ' KB';
|
|
} else {
|
|
return Math.floor(value) + ' B';
|
|
}
|
|
if (val >= 100) {
|
|
return Math.floor(val) + unit;
|
|
}
|
|
return floorFixed(val, 1).replace(/\.0$/, '') + unit;
|
|
}
|
|
|
|
function formatUserDisplayName(user) {
|
|
if (!user) {
|
|
return { display: '', full: '' };
|
|
}
|
|
|
|
var rawName = '';
|
|
if (user.nickname && user.nickname.trim() !== '') {
|
|
rawName = user.nickname;
|
|
} else if (user.hostname && user.hostname.trim() !== '') {
|
|
rawName = user.hostname;
|
|
} else {
|
|
rawName = user.mac || '';
|
|
}
|
|
|
|
return formatUserName(rawName, user.mac || '');
|
|
}
|
|
|
|
var DEVICE_ICON_COLORS = ['#3b82f6','#10b981','#f59e0b','#ef4444','#8b5cf6','#06b6d4','#f97316','#ec4899','#84cc16','#6366f1'];
|
|
function deviceIconColor(value) {
|
|
value = value || '';
|
|
var hash = 0;
|
|
for (var i = 0; i < value.length; i++) {
|
|
hash = value.charCodeAt(i) + ((hash << 5) - hash);
|
|
}
|
|
return DEVICE_ICON_COLORS[Math.abs(hash) % DEVICE_ICON_COLORS.length];
|
|
}
|
|
|
|
function buildDashboardDeviceIconHtml(displayName, mac) {
|
|
var source = displayName || mac || '?';
|
|
var firstChar = '?';
|
|
if (source && source !== '--') {
|
|
firstChar = String(source).trim().charAt(0).toUpperCase() || '?';
|
|
}
|
|
return '<span class="dashboard-device-icon" style="background:' + deviceIconColor(mac || source) + ';">' + escapeHtml(firstChar) + '</span>';
|
|
}
|
|
|
|
function normalizeMacKey(mac) {
|
|
return mac ? String(mac).trim().toLowerCase() : '';
|
|
}
|
|
|
|
function cacheUsersByMac(users) {
|
|
if (!Array.isArray(users)) {
|
|
return;
|
|
}
|
|
|
|
users.forEach(function(user) {
|
|
var key = normalizeMacKey(user && user.mac);
|
|
if (!key) {
|
|
return;
|
|
}
|
|
|
|
activeUsersByMac[key] = $.extend({}, activeUsersByMac[key] || {}, user);
|
|
});
|
|
}
|
|
|
|
function getCachedUserByMac(mac) {
|
|
var key = normalizeMacKey(mac);
|
|
return key ? (activeUsersByMac[key] || null) : null;
|
|
}
|
|
|
|
function truncateMiddle(value, maxLength) {
|
|
var text = value ? String(value).trim() : '';
|
|
maxLength = maxLength || 16;
|
|
|
|
if (text.length <= maxLength) {
|
|
return text;
|
|
}
|
|
|
|
var keepLength = maxLength - 3;
|
|
var frontLength = Math.ceil(keepLength / 2);
|
|
var backLength = keepLength - frontLength;
|
|
return text.substring(0, frontLength) + '...' + text.substring(text.length - backLength);
|
|
}
|
|
|
|
function formatActiveAppUser(app) {
|
|
app = app || {};
|
|
var mac = app.mac ? String(app.mac).trim() : '';
|
|
var cachedUser = getCachedUserByMac(mac) || {};
|
|
var nickname = app.nickname || cachedUser.nickname || '';
|
|
var hostname = app.hostname || cachedUser.hostname || '';
|
|
var rawName = '';
|
|
|
|
if (nickname && String(nickname).trim() !== '') {
|
|
rawName = String(nickname).trim();
|
|
} else if (hostname && String(hostname).trim() !== '') {
|
|
rawName = String(hostname).trim();
|
|
}
|
|
|
|
if (rawName) {
|
|
return {
|
|
display: truncateMiddle(rawName, 16) + (mac ? ' (' + mac + ')' : ''),
|
|
full: rawName + (mac ? ' (' + mac + ')' : '')
|
|
};
|
|
}
|
|
|
|
return {
|
|
display: mac || '--',
|
|
full: mac || '--'
|
|
};
|
|
}
|
|
|
|
function getUserDisplayName(user) {
|
|
var nameInfo = formatUserDisplayName(user);
|
|
return nameInfo.display;
|
|
}
|
|
|
|
function renderClientList() {
|
|
$activeTerminalList.empty();
|
|
if (!activeUsersData || activeUsersData.length === 0) {
|
|
$activeTerminalList.html('<div class="active-terminal-empty">' + _NoData + '</div>');
|
|
return;
|
|
}
|
|
|
|
var rows = activeUsersData.slice(0, 8).map(function(user) {
|
|
var nameInfo = formatUserDisplayName(user);
|
|
var name = nameInfo.display;
|
|
var fullName = nameInfo.full;
|
|
var upRate = formatRate(user.up_rate || 0);
|
|
var downRate = formatRate(user.down_rate || 0);
|
|
var app = (user.app && user.app.trim() !== '') ? user.app : '--';
|
|
var mac = user.mac || '';
|
|
var alias = (user.nickname && String(user.nickname).trim() !== '') ? user.nickname : ((user.hostname && String(user.hostname).trim() !== '') ? user.hostname : '');
|
|
var macDisplay = mac;
|
|
if (alias) {
|
|
macDisplay = mac + ' (' + alias + ')';
|
|
}
|
|
var tooltipHtml =
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _MACAddress + ':</span><span class="tooltip-value">' + escapeHtml(macDisplay) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _IP + ':</span><span class="tooltip-value">' + escapeHtml(user.ip || '--') + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _UpstreamRate + ':</span><span class="tooltip-value">' + escapeHtml(upRate) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _DownstreamRate + ':</span><span class="tooltip-value">' + escapeHtml(downRate) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _UpstreamTraffic + ':</span><span class="tooltip-value">' + escapeHtml(formatTraffic(user.today_up_bytes || 0)) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _DownstreamTraffic + ':</span><span class="tooltip-value">' + escapeHtml(formatTraffic(user.today_down_bytes || 0)) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _CurrentApp + ':</span><span class="tooltip-value">' + escapeHtml(app) + '</span></div>' +
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _CurrentURL + ':</span><span class="tooltip-value">' + escapeHtml(user.url || '--') + '</span></div>';
|
|
|
|
return '<div class="active-terminal-item">' +
|
|
'<span class="active-terminal-name">' + escapeHtml(name || '--') + '</span>' +
|
|
'<span class="active-terminal-up">↑ ' + escapeHtml(upRate) + '</span>' +
|
|
'<span class="active-terminal-down">↓ ' + escapeHtml(downRate) + '</span>' +
|
|
'<span class="active-terminal-app">' + escapeHtml(app) + '</span>' +
|
|
'<span class="tooltip">' + tooltipHtml + '</span>' +
|
|
'</div>';
|
|
}).join('');
|
|
$activeTerminalList.html(rows);
|
|
$activeTerminalList.off('mousemove.activeTip mouseleave.activeTip')
|
|
.on('mousemove.activeTip', '.active-terminal-item', function(e) {
|
|
var $tip = $(this).find('.tooltip');
|
|
if (!$tip.length) {
|
|
return;
|
|
}
|
|
var rect = this.getBoundingClientRect();
|
|
var y = e.clientY - rect.top - 10;
|
|
if (y < 0) {
|
|
y = 0;
|
|
}
|
|
$tip.css({ top: y });
|
|
})
|
|
.on('mouseleave.activeTip', '.active-terminal-item', function() {
|
|
$(this).find('.tooltip').hide();
|
|
});
|
|
}
|
|
function formatTime(timestamp){
|
|
var d = new Date(timestamp);
|
|
return d.getFullYear()+'-'+(d.getMonth()+1).toString().padStart(2,'0')+'-'+d.getDate().toString().padStart(2,'0')+' '+
|
|
d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')+':'+d.getSeconds().toString().padStart(2,'0');
|
|
}
|
|
function updateActiveApps(appList){
|
|
var $container = $('#active-apps');
|
|
|
|
if(!$container.length) return;
|
|
|
|
if(!Array.isArray(appList)) {
|
|
appList = [];
|
|
}
|
|
|
|
$container.empty();
|
|
|
|
if(!appList || appList.length === 0) {
|
|
var $noData = $('<div>').addClass('no-data').css({
|
|
'display': 'flex',
|
|
'align-items': 'center',
|
|
'justify-content': 'center',
|
|
'width': '100%',
|
|
'min-height': '60px',
|
|
'color': '#888',
|
|
'font-size': '14px'
|
|
}).text(_NoData);
|
|
$container.append($noData);
|
|
return;
|
|
}
|
|
|
|
function formatActiveAppName(name) {
|
|
var text = name ? String(name) : '';
|
|
var maxUnits = 8;
|
|
var units = 0;
|
|
var output = '';
|
|
|
|
for (var i = 0; i < text.length; i++) {
|
|
var ch = text.charAt(i);
|
|
var chUnits = /[\u4e00-\u9fa5]/.test(ch) ? 2 : 1;
|
|
if (units + chUnits > maxUnits) {
|
|
return output + '...';
|
|
}
|
|
output += ch;
|
|
units += chUnits;
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
var displayList = appList.slice(0, 10);
|
|
displayList.forEach(function(app){
|
|
var $appItem = $('<div>').addClass('app-item');
|
|
var $iconWrap = $('<span>').addClass('app-icon-wrap')
|
|
.attr('title', app.name || 'app');
|
|
var appId = (app && app.id !== undefined && app.id !== null) ? String(app.id).trim() : '';
|
|
var appDisplayName = app.name || ('app' + app.id) || '?';
|
|
var $appIcon = window.OAFIcon
|
|
? window.OAFIcon.createAppIcon(appId, appDisplayName, '/luci-static/resources', { className: 'app-icon-letter', size: '100%', radius: 'inherit', fontSize: '16px', icon: app.icon })
|
|
: $('<div>').addClass('app-icon-letter').text(appDisplayName.charAt(0).toUpperCase());
|
|
$iconWrap.append($appIcon);
|
|
var appFullName = app.name || ('app' + app.id);
|
|
var $nameSpan = $('<div>')
|
|
.addClass('app-name')
|
|
.text(formatActiveAppName(appFullName))
|
|
.attr('title', appFullName);
|
|
var $tooltip = $('<div>').addClass('tooltip tooltip-top tooltip-arrow-center tooltip-center-x');
|
|
|
|
var timestamp = app.timestamp ? formatTime(app.timestamp * 1000) : formatTime(Date.now());
|
|
var userInfo = formatActiveAppUser(app);
|
|
var tooltipHtml =
|
|
'<div class="tooltip-row"><span class="tooltip-label">' + _AccessTime + ':</span><span class="tooltip-value">'+timestamp+'</span></div>';
|
|
tooltipHtml += '<div class="tooltip-row"><span class="tooltip-label">' + _User + ':</span><span class="tooltip-value" title="' + escapeHtml(userInfo.full) + '">' + escapeHtml(userInfo.display) + '</span></div>';
|
|
if(app.src_ip) tooltipHtml += '<div class="tooltip-row"><span class="tooltip-label">' + _SourceIP + ':</span><span class="tooltip-value">'+app.src_ip+'</span></div>';
|
|
if(app.dst_ip) tooltipHtml += '<div class="tooltip-row"><span class="tooltip-label">' + _DestinationIP + ':</span><span class="tooltip-value">'+app.dst_ip+'</span></div>';
|
|
if(app.protocol) tooltipHtml += '<div class="tooltip-row"><span class="tooltip-label">' + _Protocol + ':</span><span class="tooltip-value">'+app.protocol+'</span></div>';
|
|
if(app.src_port) tooltipHtml += '<div class="tooltip-row"><span class="tooltip-label">' + _SourcePort + ':</span><span class="tooltip-value">'+app.src_port+'</span></div>';
|
|
if(app.dst_port) tooltipHtml += '<div class="tooltip-row"><span class="tooltip-label">' + _DestinationPort + ':</span><span class="tooltip-value">'+app.dst_port+'</span></div>';
|
|
if(app.domain) tooltipHtml += '<div class="tooltip-row"><span class="tooltip-label">' + _MatchedDomain + ':</span><span class="tooltip-value">'+app.domain+'</span></div>';
|
|
$tooltip.html(tooltipHtml);
|
|
$appItem.append($iconWrap).append($nameSpan).append($tooltip);
|
|
$container.append($appItem);
|
|
});
|
|
}
|
|
|
|
function extractDomain(url) {
|
|
if (!url || typeof url !== 'string') return '';
|
|
var domain = url.replace(/^https?:\/\//i, '');
|
|
var parts = domain.split('/');
|
|
return parts[0];
|
|
}
|
|
|
|
function isHttps(url) {
|
|
if (!url || typeof url !== 'string') return false;
|
|
return url.toLowerCase().startsWith('https://');
|
|
}
|
|
|
|
function truncateUrl(url, maxLength) {
|
|
if (!url || typeof url !== 'string') return '';
|
|
if (url.length <= maxLength) return url;
|
|
|
|
var frontLength = 12;
|
|
var backLength = 12;
|
|
var front = url.substring(0, frontLength);
|
|
var back = url.substring(url.length - backLength);
|
|
return front + '...' + back;
|
|
}
|
|
|
|
function isMacAddress(str) {
|
|
if (!str || typeof str !== 'string') return false;
|
|
var macPattern = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/;
|
|
return macPattern.test(str);
|
|
}
|
|
|
|
function formatMacAddress(mac) {
|
|
if (!mac || typeof mac !== 'string') return '';
|
|
if (!isMacAddress(mac)) return mac;
|
|
|
|
var parts = mac.split(':');
|
|
if (parts.length >= 3) {
|
|
return parts.slice(-3).join(':');
|
|
}
|
|
return mac;
|
|
}
|
|
|
|
function hasChinese(str) {
|
|
if (!str || typeof str !== 'string') return false;
|
|
return /[\u4e00-\u9fa5]/.test(str);
|
|
}
|
|
|
|
function formatUserName(name, mac) {
|
|
var displayName = name || mac || '';
|
|
var fullName = displayName;
|
|
|
|
if (isMacAddress(displayName)) {
|
|
displayName = formatMacAddress(displayName);
|
|
} else if (displayName) {
|
|
if (hasChinese(displayName)) {
|
|
var chineseChars = displayName.match(/[\u4e00-\u9fa5]/g);
|
|
if (chineseChars && chineseChars.length > 5) {
|
|
var count = 0;
|
|
var index = 0;
|
|
for (var i = 0; i < displayName.length; i++) {
|
|
if (/[\u4e00-\u9fa5]/.test(displayName[i])) {
|
|
count++;
|
|
if (count === 5) {
|
|
index = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
displayName = displayName.substring(0, index) + '...';
|
|
}
|
|
} else {
|
|
if (displayName.length > 12) {
|
|
var front = displayName.substring(0, 6);
|
|
var back = displayName.substring(displayName.length - 6);
|
|
displayName = front + '...' + back;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
display: displayName,
|
|
full: fullName
|
|
};
|
|
}
|
|
|
|
function formatActiveUrlUserName(name, mac) {
|
|
var displayName = name || mac || '';
|
|
return {
|
|
display: isMacAddress(displayName) ? formatMacAddress(displayName) : displayName,
|
|
full: displayName
|
|
};
|
|
}
|
|
|
|
function setMiddleEllipsisText($container, text) {
|
|
var value = text || '--';
|
|
$container.empty();
|
|
if (value === '--' || value.length <= 28) {
|
|
$container.text(value);
|
|
return;
|
|
}
|
|
var suffixLength = Math.min(18, Math.floor(value.length / 2));
|
|
$container.append(
|
|
$('<span>').addClass('active-url-url-prefix').text(value.substring(0, value.length - suffixLength)),
|
|
$('<span>').addClass('active-url-url-suffix').text(value.substring(value.length - suffixLength))
|
|
);
|
|
}
|
|
|
|
function getUrlProtocol(url) {
|
|
if (!url || typeof url !== 'string') return '';
|
|
if (url.toLowerCase().startsWith('https://')) return 'https://';
|
|
if (url.toLowerCase().startsWith('http://')) return 'http://';
|
|
return '';
|
|
}
|
|
|
|
function formatActiveHostUrl(host) {
|
|
var rawUrl = (host && (host.url || host.host)) ? String(host.url || host.host).trim() : '';
|
|
if (!rawUrl) {
|
|
return '';
|
|
}
|
|
|
|
if (getUrlProtocol(rawUrl)) {
|
|
return rawUrl;
|
|
}
|
|
|
|
if (parseInt(host.app_proto, 10) === 1 || parseInt(host.dst_port, 10) === 80) {
|
|
return 'http://' + rawUrl;
|
|
}
|
|
|
|
return 'https://' + rawUrl;
|
|
}
|
|
|
|
function startsWithWww(url) {
|
|
if (!url || typeof url !== 'string') return false;
|
|
var withoutProtocol = url.replace(/^https?:\/\//i, '');
|
|
return withoutProtocol.toLowerCase().startsWith('www.');
|
|
}
|
|
|
|
var cachedHostKeys = [];
|
|
var MAX_DISPLAY_HOSTS = 10;
|
|
var pendingHostQueue = [];
|
|
var isRenderingHosts = false;
|
|
var hostRenderInterval = null;
|
|
var isFirstRender = true;
|
|
|
|
var userColors = [
|
|
'#38bdf8',
|
|
'#f97316',
|
|
'#c084fc',
|
|
'#ec4899',
|
|
'#84cc16'
|
|
];
|
|
|
|
function getUserColor(mac) {
|
|
if (!mac) return userColors[0];
|
|
|
|
var hash = 0;
|
|
for (var i = 0; i < mac.length; i++) {
|
|
hash = ((hash * 31) + mac.charCodeAt(i)) >>> 0;
|
|
}
|
|
|
|
var colorIndex = hash % userColors.length;
|
|
return userColors[colorIndex];
|
|
}
|
|
|
|
function createHostItem(host) {
|
|
var $item = $('<div>').addClass('active-url-item');
|
|
|
|
var fullUrl = formatActiveHostUrl(host);
|
|
var userName = host.name || host.mac || '';
|
|
|
|
if (startsWithWww(fullUrl)) {
|
|
$item.addClass('www-highlight');
|
|
}
|
|
|
|
var displayUrl = fullUrl || '--';
|
|
|
|
var nameInfo = formatActiveUrlUserName(host.name, host.mac);
|
|
var displayName = nameInfo.display;
|
|
var fullName = nameInfo.full;
|
|
|
|
var userColor = getUserColor(host.mac);
|
|
|
|
var $name = $('<span>').addClass('active-url-name');
|
|
if (displayName) {
|
|
$name.text(displayName + ': ');
|
|
$name.css('color', userColor);
|
|
if (displayName !== fullName) {
|
|
$name.attr('title', fullName);
|
|
}
|
|
}
|
|
var $url = $('<span>').addClass('active-url-url');
|
|
setMiddleEllipsisText($url, displayUrl);
|
|
|
|
$item.append($name).append($url);
|
|
|
|
var titleParts = [];
|
|
if (fullName) {
|
|
titleParts.push(fullName);
|
|
}
|
|
if (fullUrl) {
|
|
titleParts.push(fullUrl);
|
|
}
|
|
if (titleParts.length > 0) {
|
|
$item.attr('title', titleParts.join(': '));
|
|
}
|
|
|
|
var hostKey = (host.mac || '') + '|' + (fullUrl || '') + '|' + (host.timestamp || 0);
|
|
$item.data('host-key', hostKey);
|
|
|
|
return $item;
|
|
}
|
|
|
|
function getHostKey(host) {
|
|
var fullUrl = host.url || host.host || '';
|
|
return (host.mac || '') + '|' + fullUrl + '|' + (host.timestamp || 0);
|
|
}
|
|
|
|
function renderNextHostItem(noAnimation) {
|
|
var $container = $('#active-url-list');
|
|
|
|
if(!$container.length || pendingHostQueue.length === 0) {
|
|
isRenderingHosts = false;
|
|
if(hostRenderInterval) {
|
|
clearInterval(hostRenderInterval);
|
|
hostRenderInterval = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
var host = pendingHostQueue.shift();
|
|
var $item = createHostItem(host);
|
|
|
|
if(!noAnimation && !isFirstRender) {
|
|
$item.addClass('slide-in');
|
|
setTimeout(function() {
|
|
$item.removeClass('slide-in');
|
|
}, 400);
|
|
}
|
|
|
|
$container.append($item);
|
|
|
|
var key = getHostKey(host);
|
|
cachedHostKeys.push(key);
|
|
|
|
var $items = $container.find('.active-url-item');
|
|
if($items.length > MAX_DISPLAY_HOSTS) {
|
|
var $oldestItem = $items.first();
|
|
var oldKey = $oldestItem.data('host-key');
|
|
var keyIndex = cachedHostKeys.indexOf(oldKey);
|
|
if(keyIndex !== -1) {
|
|
cachedHostKeys.splice(keyIndex, 1);
|
|
}
|
|
$oldestItem.remove();
|
|
}
|
|
}
|
|
|
|
function startHostRendering(newHosts) {
|
|
if(!newHosts || newHosts.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if(isRenderingHosts) {
|
|
pendingHostQueue = pendingHostQueue.concat(newHosts);
|
|
return;
|
|
}
|
|
|
|
var $container = $('#active-url-list');
|
|
var isFirstTime = isFirstRender && $container.find('.active-url-item').length === 0;
|
|
|
|
isRenderingHosts = true;
|
|
pendingHostQueue = newHosts.slice();
|
|
|
|
if(hostRenderInterval) {
|
|
clearInterval(hostRenderInterval);
|
|
hostRenderInterval = null;
|
|
}
|
|
|
|
if(isFirstTime) {
|
|
while(pendingHostQueue.length > 0) {
|
|
renderNextHostItem(true);
|
|
}
|
|
isFirstRender = false;
|
|
isRenderingHosts = false;
|
|
} else {
|
|
renderNextHostItem(false);
|
|
|
|
hostRenderInterval = setInterval(function() {
|
|
renderNextHostItem(false);
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
function updateActiveHosts(hostList){
|
|
var $container = $('#active-url-list');
|
|
|
|
if(!$container.length) return;
|
|
|
|
if(!Array.isArray(hostList)) {
|
|
hostList = [];
|
|
}
|
|
|
|
$container.find('.active-url-header').remove();
|
|
|
|
var currentKeys = [];
|
|
$container.find('.active-url-item').each(function() {
|
|
var key = $(this).data('host-key');
|
|
if (key) {
|
|
currentKeys.push(key);
|
|
}
|
|
});
|
|
|
|
var newHosts = [];
|
|
if(hostList && hostList.length > 0) {
|
|
hostList.forEach(function(newHost) {
|
|
var newKey = getHostKey(newHost);
|
|
if(currentKeys.indexOf(newKey) === -1 && cachedHostKeys.indexOf(newKey) === -1) {
|
|
newHosts.push(newHost);
|
|
}
|
|
});
|
|
}
|
|
|
|
if(newHosts.length === 0 && currentKeys.length === 0 && !isRenderingHosts) {
|
|
$container.children().not('.active-url-header').remove();
|
|
var $noData = $('<div>').addClass('no-data').css({
|
|
'display': 'flex',
|
|
'align-items': 'center',
|
|
'justify-content': 'center',
|
|
'width': '100%',
|
|
'min-height': '60px',
|
|
'color': '#888',
|
|
'font-size': '14px',
|
|
'padding': '20px'
|
|
}).text(_NoData);
|
|
$container.append($noData);
|
|
return;
|
|
}
|
|
|
|
if(newHosts.length > 0) {
|
|
$container.find('.no-data').remove();
|
|
startHostRendering(newHosts);
|
|
}
|
|
}
|
|
|
|
function updateInterfaceTraffic(trafficData){
|
|
if(!lineWan) return;
|
|
if(!trafficData || !trafficData.interface || !trafficData.traffic) return;
|
|
|
|
var newInterface = trafficData.interface || '';
|
|
if(newInterface !== currentInterface){
|
|
currentInterface = newInterface;
|
|
updateTrafficTitle(currentInterface);
|
|
}
|
|
|
|
var upData = [];
|
|
var downData = [];
|
|
trafficData.traffic.forEach(function(item){
|
|
upData.push(item.up || 0);
|
|
downData.push(item.down || 0);
|
|
});
|
|
|
|
while(upData.length < TRAFFIC_SAMPLE_POINTS){
|
|
upData.unshift(0);
|
|
downData.unshift(0);
|
|
}
|
|
if(upData.length > TRAFFIC_SAMPLE_POINTS){
|
|
upData = upData.slice(-TRAFFIC_SAMPLE_POINTS);
|
|
downData = downData.slice(-TRAFFIC_SAMPLE_POINTS);
|
|
}
|
|
|
|
var dataChanged = false;
|
|
if(!up || up.length !== upData.length){
|
|
dataChanged = true;
|
|
} else {
|
|
for(var i = 0; i < upData.length; i++){
|
|
if(up[i] !== upData[i] || down[i] !== downData[i]){
|
|
dataChanged = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(dataChanged){
|
|
var maxValue = 0;
|
|
for(var i = 0; i < upData.length; i++){
|
|
maxValue = Math.max(maxValue, upData[i], downData[i]);
|
|
}
|
|
|
|
var interval = calculateYAxisInterval(maxValue);
|
|
|
|
var suggestedMax = interval * 6;
|
|
|
|
up = upData;
|
|
down = downData;
|
|
lineWan.setOption({
|
|
yAxis: {
|
|
interval: interval,
|
|
max: suggestedMax
|
|
},
|
|
series:[
|
|
{data:up},
|
|
{data:down}
|
|
],
|
|
animation: false,
|
|
animationDurationUpdate: 0
|
|
}, { notMerge: false, lazyUpdate: false });
|
|
}
|
|
|
|
var lastUp = upData.length > 0 ? upData[upData.length - 1] : 0;
|
|
var lastDown = downData.length > 0 ? downData[downData.length - 1] : 0;
|
|
updateWanRealtimeRate(lastUp, lastDown);
|
|
}
|
|
|
|
function updateHistoryTrafficChart(data, type){
|
|
type = type || 'today';
|
|
var list = [];
|
|
var labels = [];
|
|
var upData = [];
|
|
var downData = [];
|
|
var labelInterval = 0;
|
|
|
|
if (type === 'days30') {
|
|
list = data && Array.isArray(data.list) ? data.list : [];
|
|
labelInterval = Math.max(0, Math.floor(list.length / 8));
|
|
|
|
list.forEach(function(item){
|
|
var date = item.date_str || '';
|
|
labels.push(date.length >= 10 ? date.slice(5) : date);
|
|
upData.push(item.up_bytes || 0);
|
|
downData.push(item.down_bytes || 0);
|
|
});
|
|
} else {
|
|
var hourlyList = data && Array.isArray(data.hourly_traffic) ? data.hourly_traffic : [];
|
|
var hourMap = {};
|
|
labelInterval = 2;
|
|
|
|
hourlyList.forEach(function(item, index) {
|
|
var hourValue = parseInt(item && item.hour, 10);
|
|
if (!isNaN(hourValue) && hourValue >= 0 && hourValue < 24) {
|
|
hourMap[hourValue] = item;
|
|
} else {
|
|
hourMap[index] = item;
|
|
}
|
|
});
|
|
|
|
for (var hour = 0; hour < 24; hour++) {
|
|
var item = hourMap[hour] || {};
|
|
var traffic = item.traffic || {};
|
|
var hourLabel = (hour < 10 ? '0' : '') + hour + ':00';
|
|
var upBytes = traffic.up_bytes || item.up_bytes || 0;
|
|
var downBytes = traffic.down_bytes || item.down_bytes || 0;
|
|
labels.push(hourLabel);
|
|
upData.push(upBytes);
|
|
downData.push(downBytes);
|
|
list.push({
|
|
date_str: hourLabel,
|
|
up_bytes: upBytes,
|
|
down_bytes: downBytes,
|
|
total_bytes: upBytes + downBytes
|
|
});
|
|
}
|
|
}
|
|
|
|
var maxValue = 0;
|
|
upData.forEach(function(v) {
|
|
if (v > maxValue) { maxValue = v; }
|
|
});
|
|
downData.forEach(function(v) {
|
|
if (v > maxValue) { maxValue = v; }
|
|
});
|
|
var trafficInterval = calculateYAxisInterval(maxValue);
|
|
var trafficMax = Math.ceil(maxValue / trafficInterval) * trafficInterval;
|
|
if (trafficMax <= 0) {
|
|
trafficMax = trafficInterval * 6;
|
|
}
|
|
|
|
historyTraffic.setOption({
|
|
backgroundColor: 'transparent',
|
|
color: [userTrafficUpColor, userTrafficDownColor],
|
|
textStyle: { color: axisLabelColor },
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
axisPointer: {
|
|
type: 'line',
|
|
lineStyle: {
|
|
color: splitLineDashedColor,
|
|
width: 1
|
|
}
|
|
},
|
|
backgroundColor: tooltipBg,
|
|
textStyle: { color: tooltipText },
|
|
formatter: function(params) {
|
|
var index = params && params.length ? params[0].dataIndex : 0;
|
|
var item = list[index] || {};
|
|
var traffic = item.traffic || {};
|
|
var label = type === 'days30' ? (item.date_str || labels[index] || '') : (labels[index] || '');
|
|
var up = traffic.up_bytes || item.up_bytes || 0;
|
|
var down = traffic.down_bytes || item.down_bytes || 0;
|
|
return label + '<br/>' +
|
|
_Upstream + ': ' + formatTraffic(up) + '<br/>' +
|
|
_Downstream + ': ' + formatTraffic(down) + '<br/>' +
|
|
_Total + ': ' + formatTraffic(item.total_bytes || (up + down));
|
|
}
|
|
},
|
|
legend: {
|
|
top: 0,
|
|
textStyle: { color: axisLabelColor }
|
|
},
|
|
grid: {
|
|
left: '2%',
|
|
right: '3%',
|
|
top: 34,
|
|
bottom: 24,
|
|
containLabel: true
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
data: labels,
|
|
axisLabel: {
|
|
color: axisLabelColor,
|
|
interval: labelInterval
|
|
},
|
|
axisLine: axis.axisLine,
|
|
splitLine: { show: false }
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
interval: trafficInterval,
|
|
max: trafficMax,
|
|
axisLabel: {
|
|
color: axisLabelColor,
|
|
formatter: function(value) {
|
|
return formatTraffic(value);
|
|
}
|
|
},
|
|
axisLine: axis.axisLine,
|
|
splitLine: {
|
|
show: true,
|
|
lineStyle: {
|
|
type: 'dashed',
|
|
color: splitLineDashedColor,
|
|
width: 1
|
|
}
|
|
}
|
|
},
|
|
series: [
|
|
{
|
|
name: _Upstream,
|
|
type: 'line',
|
|
smooth: true,
|
|
symbol: 'circle',
|
|
symbolSize: 5,
|
|
showSymbol: type === 'days30',
|
|
lineStyle: {
|
|
width: 1.3
|
|
},
|
|
areaStyle: {
|
|
opacity: 0.08
|
|
},
|
|
data: upData
|
|
},
|
|
{
|
|
name: _Downstream,
|
|
type: 'line',
|
|
smooth: true,
|
|
symbol: 'circle',
|
|
symbolSize: 5,
|
|
showSymbol: type === 'days30',
|
|
lineStyle: {
|
|
width: 1.3
|
|
},
|
|
areaStyle: {
|
|
opacity: 0.08
|
|
},
|
|
data: downData
|
|
}
|
|
]
|
|
});
|
|
resizeDashboardCharts();
|
|
}
|
|
|
|
function updateUserTrafficChart(data){
|
|
if(!data || !data.users || !Array.isArray(data.users)) {
|
|
barUserTraffic.setOption({
|
|
yAxis: [
|
|
{ data: [] },
|
|
{ data: [] }
|
|
],
|
|
series: [
|
|
{ name: _Upstream, data: [] },
|
|
{ name: _Downstream, data: [] }
|
|
]
|
|
});
|
|
resizeDashboardCharts();
|
|
return;
|
|
}
|
|
|
|
var users = data.users || [];
|
|
cacheUsersByMac(users);
|
|
var names = [];
|
|
var upValues = [];
|
|
var downValues = [];
|
|
|
|
users.forEach(function(user){
|
|
var nameInfo = formatUserDisplayName(user);
|
|
var displayName = nameInfo.display || _Unknown;
|
|
var fullName = nameInfo.full;
|
|
names.push(displayName);
|
|
|
|
var userInfo = {
|
|
name: displayName,
|
|
fullName: fullName,
|
|
mac: user.mac || '',
|
|
ip: user.ip || '',
|
|
hostname: user.hostname || '',
|
|
nickname: user.nickname || '',
|
|
up_bytes: user.up_bytes || 0,
|
|
down_bytes: user.down_bytes || 0,
|
|
total_bytes: user.total_bytes || 0,
|
|
_rawUser: user
|
|
};
|
|
|
|
upValues.push({
|
|
value: user.up_bytes || 0,
|
|
user: userInfo
|
|
});
|
|
|
|
downValues.push({
|
|
value: user.down_bytes || 0,
|
|
user: userInfo
|
|
});
|
|
});
|
|
|
|
names.reverse();
|
|
upValues.reverse();
|
|
downValues.reverse();
|
|
|
|
var userCount = users.length;
|
|
var gridConfig = {
|
|
left: 8,
|
|
right: 8,
|
|
top: '2%',
|
|
bottom: '12%',
|
|
containLabel: true
|
|
};
|
|
|
|
var maxTraffic = 0;
|
|
users.forEach(function(user) {
|
|
var total = (user.up_bytes || 0) + (user.down_bytes || 0);
|
|
if (total > maxTraffic) {
|
|
maxTraffic = total;
|
|
}
|
|
});
|
|
|
|
var calculatedMax = maxTraffic * 1.15;
|
|
|
|
var boundaryGapConfig = [0, 0];
|
|
var barCategoryGapValue = '50%';
|
|
|
|
if (userCount <= 3) {
|
|
boundaryGapConfig = [0, 0.5];
|
|
barCategoryGapValue = 10;
|
|
} else {
|
|
boundaryGapConfig = [0, 0];
|
|
barCategoryGapValue = '50%';
|
|
}
|
|
|
|
barUserTraffic.setOption({
|
|
grid: gridConfig,
|
|
xAxis: {
|
|
min: 0,
|
|
max: calculatedMax
|
|
},
|
|
yAxis: [
|
|
{
|
|
data: names,
|
|
boundaryGap: boundaryGapConfig,
|
|
axisLabel: {
|
|
formatter: function(value, index) {
|
|
return (names.length - index).toString();
|
|
}
|
|
}
|
|
},
|
|
{
|
|
data: names,
|
|
boundaryGap: boundaryGapConfig,
|
|
axisLabel: {
|
|
show: false
|
|
}
|
|
}
|
|
],
|
|
series: [
|
|
{
|
|
name: _Upstream,
|
|
data: upValues,
|
|
barWidth: 25,
|
|
barCategoryGap: barCategoryGapValue,
|
|
label: {
|
|
show: false
|
|
}
|
|
},
|
|
{
|
|
name: _Downstream,
|
|
data: downValues,
|
|
barWidth: 25,
|
|
barCategoryGap: barCategoryGapValue,
|
|
label: {
|
|
show: true,
|
|
position: 'right',
|
|
color: axisLabelColor,
|
|
fontSize: 11,
|
|
formatter: function(params) {
|
|
if (params.data.user) {
|
|
var totalBytes = (params.data.user.up_bytes || 0) + (params.data.user.down_bytes || 0);
|
|
var totalTraffic = formatTraffic(totalBytes);
|
|
|
|
var maxValue = calculatedMax;
|
|
var percentage = totalBytes / maxValue;
|
|
|
|
var rawUser = params.data.user._rawUser || params.data.user;
|
|
var rawName = '';
|
|
if (rawUser.nickname && rawUser.nickname.trim() !== '') {
|
|
rawName = rawUser.nickname;
|
|
} else if (rawUser.hostname && rawUser.hostname.trim() !== '') {
|
|
rawName = rawUser.hostname;
|
|
} else {
|
|
rawName = rawUser.mac || '';
|
|
}
|
|
|
|
var displayName = rawName;
|
|
|
|
if (percentage < 0.5) {
|
|
if (isMacAddress(displayName)) {
|
|
displayName = formatMacAddress(displayName);
|
|
} else if (displayName) {
|
|
if (hasChinese(displayName)) {
|
|
var chineseChars = displayName.match(/[\u4e00-\u9fa5]/g);
|
|
if (chineseChars && chineseChars.length > 10) {
|
|
var count = 0;
|
|
var index = 0;
|
|
for (var i = 0; i < displayName.length; i++) {
|
|
if (/[\u4e00-\u9fa5]/.test(displayName[i])) {
|
|
count++;
|
|
if (count === 10) {
|
|
index = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
displayName = displayName.substring(0, index) + '...';
|
|
}
|
|
} else {
|
|
if (displayName.length > 20) {
|
|
displayName = displayName.substring(0, 17) + '...';
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (isMacAddress(displayName)) {
|
|
displayName = formatMacAddress(displayName);
|
|
} else if (displayName) {
|
|
if (hasChinese(displayName)) {
|
|
var chineseChars = displayName.match(/[\u4e00-\u9fa5]/g);
|
|
if (chineseChars && chineseChars.length > 5) {
|
|
var count = 0;
|
|
var index = 0;
|
|
for (var i = 0; i < displayName.length; i++) {
|
|
if (/[\u4e00-\u9fa5]/.test(displayName[i])) {
|
|
count++;
|
|
if (count === 5) {
|
|
index = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
displayName = displayName.substring(0, index) + '...';
|
|
}
|
|
} else {
|
|
if (displayName.length > 12) {
|
|
var front = displayName.substring(0, 6);
|
|
var back = displayName.substring(displayName.length - 6);
|
|
displayName = front + '...' + back;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return displayName + '(' + totalTraffic + ')';
|
|
}
|
|
return '';
|
|
},
|
|
distance: 3,
|
|
overflow: 'truncate',
|
|
width: 130
|
|
}
|
|
}
|
|
]
|
|
});
|
|
resizeDashboardCharts();
|
|
}
|
|
|
|
|
|
//]]></script>
|