diff --git a/apps/dao/dashboard.go b/apps/dao/dashboard.go index dda0a81..114d29c 100644 --- a/apps/dao/dashboard.go +++ b/apps/dao/dashboard.go @@ -6,6 +6,7 @@ import ( "hy2xs-admin/model/constant" "hy2xs-admin/model/entity" "hy2xs-admin/model/vo" + "strings" "time" ) @@ -119,20 +120,55 @@ func DashboardTopPeers(fromMs int64, toMs int64, limit int) ([]vo.DashboardTopPe return rows, nil } -func DashboardTrafficTimeseries(fromMs int64, toMs int64) ([]vo.DashboardSeriesPointVo, error) { +func DashboardTrafficTimeseries(fromMs int64, toMs int64, bucketMs int64, source string) ([]vo.DashboardSeriesPointVo, error) { rows := make([]vo.DashboardSeriesPointVo, 0) + if toMs < fromMs { + return rows, nil + } + if bucketMs <= 0 { + bucketMs = int64(time.Minute / time.Millisecond) + } + + sourceTable := "traffic_sample" + fromColumn := "sampled_at" + switch strings.TrimSpace(strings.ToLower(source)) { + case "hourly": + sourceTable = "traffic_aggregate_hourly" + fromColumn = "hour_start" + case "daily": + sourceTable = "traffic_aggregate_daily" + fromColumn = "day_start" + default: + sourceTable = "traffic_sample" + fromColumn = "sampled_at" + } + + alignedFrom := fromMs - (fromMs % bucketMs) if tx := sqliteDB.Raw(`SELECT - hour_start AS ts, + (? + CAST((`+fromColumn+` - ?) / ? AS INTEGER) * ?) AS ts, COALESCE(SUM(rx_bytes),0) AS download, COALESCE(SUM(tx_bytes),0) AS upload - FROM traffic_aggregate_hourly - WHERE hour_start BETWEEN ? AND ? - GROUP BY hour_start - ORDER BY hour_start ASC`, fromMs, toMs).Scan(&rows); tx.Error != nil { + FROM `+sourceTable+` + WHERE `+fromColumn+` BETWEEN ? AND ? + GROUP BY ts + ORDER BY ts ASC`, alignedFrom, alignedFrom, bucketMs, bucketMs, fromMs, toMs).Scan(&rows); tx.Error != nil { logrus.Errorf("%v", tx.Error) return rows, errors.New(constant.SysError) } - return rows, nil + + rowMap := make(map[int64]vo.DashboardSeriesPointVo, len(rows)) + for _, item := range rows { + rowMap[item.Ts] = item + } + filled := make([]vo.DashboardSeriesPointVo, 0, int((toMs-alignedFrom)/bucketMs)+1) + for ts := alignedFrom; ts <= toMs; ts += bucketMs { + if item, ok := rowMap[ts]; ok { + filled = append(filled, item) + continue + } + filled = append(filled, vo.DashboardSeriesPointVo{Ts: ts, Download: 0, Upload: 0}) + } + return filled, nil } func DashboardSystemTimeseries(fromMs int64, toMs int64) ([]vo.DashboardSeriesPointVo, error) { diff --git a/apps/dao/traffic.go b/apps/dao/traffic.go index eb83720..44ba868 100644 --- a/apps/dao/traffic.go +++ b/apps/dao/traffic.go @@ -8,7 +8,6 @@ import ( "gorm.io/gorm/clause" "hy2xs-admin/model/constant" "hy2xs-admin/model/entity" - "time" ) func SaveTrafficSample(sample entity.TrafficSample) error { @@ -29,13 +28,11 @@ func UpsertTrafficAggregateHourly(peerId int64, hourStart int64, rxBytes int64, RxBytes: &rxBytes, TxBytes: &txBytes, } - now := time.Now() if tx := sqliteDB.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "peer_id"}, {Name: "hour_start"}}, DoUpdates: clause.Assignments(map[string]interface{}{ - "rx_bytes": gormExprAdd("rx_bytes", rxBytes), - "tx_bytes": gormExprAdd("tx_bytes", txBytes), - "update_time": now, + "rx_bytes": gormExprAdd("rx_bytes", rxBytes), + "tx_bytes": gormExprAdd("tx_bytes", txBytes), }), }).Create(&agg); tx.Error != nil { logrus.Errorf("%v", tx.Error) @@ -54,13 +51,11 @@ func UpsertTrafficAggregateDaily(peerId int64, dayStart int64, rxBytes int64, tx RxBytes: &rxBytes, TxBytes: &txBytes, } - now := time.Now() if tx := sqliteDB.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "peer_id"}, {Name: "day_start"}}, DoUpdates: clause.Assignments(map[string]interface{}{ - "rx_bytes": gormExprAdd("rx_bytes", rxBytes), - "tx_bytes": gormExprAdd("tx_bytes", txBytes), - "update_time": now, + "rx_bytes": gormExprAdd("rx_bytes", rxBytes), + "tx_bytes": gormExprAdd("tx_bytes", txBytes), }), }).Create(&agg); tx.Error != nil { logrus.Errorf("%v", tx.Error) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 4acab8e..f6d3c01 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -22,6 +22,7 @@ "nprogress": "^0.2.0", "path-browserify": "^1.0.1", "pinia": "^2.0.33", + "qrcode.vue": "3.4.1", "vue": "^3.2.45", "vue-echarts": "^7.0.3", "vue-i18n": "9", diff --git a/apps/frontend/pnpm-lock.yaml b/apps/frontend/pnpm-lock.yaml index 34a8690..caa5833 100644 --- a/apps/frontend/pnpm-lock.yaml +++ b/apps/frontend/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: pinia: specifier: ^2.0.33 version: 2.0.33(typescript@4.9.3)(vue@3.2.45) + qrcode.vue: + specifier: 3.4.1 + version: 3.4.1(vue@3.2.45) vue: specifier: ^3.2.45 version: 3.2.45 @@ -2272,6 +2275,11 @@ packages: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} + qrcode.vue@3.4.1: + resolution: {integrity: sha512-wq/zHsifH4FJ1GXQi8/wNxD1KfQkckIpjK1KPTc/qwYU5/Bkd4me0w4xZSg6EXk6xLBkVDE0zxVagewv5EMAVA==} + peerDependencies: + vue: ^3.0.0 + query-string@4.3.4: resolution: {integrity: sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==} engines: {node: '>=0.10.0'} @@ -5297,6 +5305,10 @@ snapshots: punycode@2.3.0: {} + qrcode.vue@3.4.1(vue@3.2.45): + dependencies: + vue: 3.2.45 + query-string@4.3.4: dependencies: object-assign: 4.1.1 diff --git a/apps/frontend/src/api/peer/types.ts b/apps/frontend/src/api/peer/types.ts index 327b825..0d40379 100644 --- a/apps/frontend/src/api/peer/types.ts +++ b/apps/frontend/src/api/peer/types.ts @@ -43,7 +43,7 @@ export interface PeerVo extends IdDto { export interface PeerClientConfigVo { url: string; - qrCode: string | Uint8Array; + qrCode?: string | Uint8Array; } export interface KickPeerDto { diff --git a/apps/frontend/src/lang/package/en.ts b/apps/frontend/src/lang/package/en.ts index ed0b29d..2ba0fa9 100644 --- a/apps/frontend/src/lang/package/en.ts +++ b/apps/frontend/src/lang/package/en.ts @@ -38,6 +38,7 @@ export default { todayUpload: "Today upload", trafficChart: "Traffic & System Timeseries", trafficSeriesChart: "Traffic: download/upload", + noTrafficData: "No traffic data for the selected period", systemSeriesChart: "System: CPU/RAM", download: "Download", upload: "Upload", diff --git a/apps/frontend/src/lang/package/ru.ts b/apps/frontend/src/lang/package/ru.ts index fb25315..22abf77 100644 --- a/apps/frontend/src/lang/package/ru.ts +++ b/apps/frontend/src/lang/package/ru.ts @@ -36,6 +36,7 @@ export default { todayUpload: "Отдано за сегодня", trafficChart: "Трафик и системные метрики", trafficSeriesChart: "Трафик: download/upload", + noTrafficData: "Нет данных трафика за выбранный период", systemSeriesChart: "Система: CPU/RAM", download: "Скачано", upload: "Отдано", diff --git a/apps/frontend/src/layout/components/Navbar.vue b/apps/frontend/src/layout/components/Navbar.vue index 56ecac3..e6eeb49 100644 --- a/apps/frontend/src/layout/components/Navbar.vue +++ b/apps/frontend/src/layout/components/Navbar.vue @@ -82,7 +82,9 @@ function logout() {
- + + +
- + +
@@ -160,14 +165,6 @@ const loadDashboard = async () => { } }; -const trafficXAxisLabels = computed(() => { - const allTs = new Set(); - for (const item of timeseries.value.traffic || []) { - allTs.add(item.ts); - } - return Array.from(allTs).sort((a, b) => a - b); -}); - const systemXAxisLabels = computed(() => { const allTs = new Set(); for (const item of timeseries.value.system || []) { @@ -176,13 +173,15 @@ const systemXAxisLabels = computed(() => { return Array.from(allTs).sort((a, b) => a - b); }); -const trafficMap = computed(() => { - const m = new Map(); - for (const item of timeseries.value.traffic || []) { - m.set(item.ts, { download: item.download || 0, upload: item.upload || 0 }); - } - return m; -}); +const trafficDownloadData = computed(() => + (timeseries.value.traffic || []).map((item) => [item.ts, item.download || 0]) +); + +const trafficUploadData = computed(() => + (timeseries.value.traffic || []).map((item) => [item.ts, item.upload || 0]) +); + +const hasSingleTrafficPoint = computed(() => (timeseries.value.traffic || []).length === 1); const systemMap = computed(() => { const m = new Map(); @@ -202,9 +201,8 @@ const trafficChartOption = computed(() => ({ grid: { left: 30, right: 20, top: 50, bottom: 50, containLabel: true }, dataZoom: [{ type: "inside" }, { type: "slider", height: 16, bottom: 10 }], xAxis: { - type: "category", + type: "time", boundaryGap: false, - data: trafficXAxisLabels.value.map((ts) => timestampToDateTime(ts)), }, yAxis: { type: "value", @@ -217,15 +215,15 @@ const trafficChartOption = computed(() => ({ name: t("dashboard.download"), type: "line", smooth: true, - showSymbol: false, - data: trafficXAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.download || 0), + showSymbol: hasSingleTrafficPoint.value, + data: trafficDownloadData.value, }, { name: t("dashboard.upload"), type: "line", smooth: true, - showSymbol: false, - data: trafficXAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.upload || 0), + showSymbol: hasSingleTrafficPoint.value, + data: trafficUploadData.value, }, ], })); @@ -323,5 +321,9 @@ onUnmounted(() => { width: 100%; height: 340px; } + +.chart-empty { + height: 340px; +} diff --git a/apps/frontend/src/views/hysteria/list/index.vue b/apps/frontend/src/views/hysteria/list/index.vue index 9fc5099..182fac4 100644 --- a/apps/frontend/src/views/hysteria/list/index.vue +++ b/apps/frontend/src/views/hysteria/list/index.vue @@ -23,7 +23,7 @@ - + {{ $t("hysteria.hysteria2Version") }}: {{ hysteria2Monitor.version ? hysteria2Monitor.version : "-" }} @@ -1251,4 +1251,11 @@ onMounted(() => { max-width: 1000px; margin: 0 auto; } + +.hysteria-version-tag { + max-width: 320px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/apps/frontend/src/views/peer/list/index.vue b/apps/frontend/src/views/peer/list/index.vue index b5b476b..cd15758 100644 --- a/apps/frontend/src/views/peer/list/index.vue +++ b/apps/frontend/src/views/peer/list/index.vue @@ -56,23 +56,27 @@ - + @@ -101,7 +105,9 @@ - +
+ +
@@ -127,8 +133,10 @@ {{ $t("peer.copyUri") }} {{ $t("peer.clientQr") }} -
- +
+
+ +
@@ -137,6 +145,7 @@