diff --git a/Cargo.lock b/Cargo.lock index 86030cb02..e36af316a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,7 +155,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", - "cpufeatures 0.2.17", + "cpufeatures", "password-hash", ] @@ -610,17 +610,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", -] - [[package]] name = "chrono" version = "0.4.44" @@ -700,16 +689,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -785,15 +764,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - [[package]] name = "crc" version = "3.4.0" @@ -1459,7 +1429,6 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -1568,70 +1537,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "hickory-net" -version = "0.26.1" +name = "hickory-proto" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" dependencies = [ "async-trait", "cfg-if", "data-encoding", + "enum-as-inner", "futures-channel", "futures-io", "futures-util", - "hickory-proto", - "idna 1.1.0", - "ipnet", - "jni", - "rand 0.10.1", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-proto" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" -dependencies = [ - "data-encoding", "idna 1.1.0", "ipnet", - "jni", "once_cell", - "prefix-trie", - "rand 0.10.1", + "rand 0.9.4", "ring", "thiserror 2.0.18", "tinyvec", + "tokio", "tracing", "url", ] [[package]] name = "hickory-resolver" -version = "0.26.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", - "hickory-net", "hickory-proto", "ipconfig", - "ipnet", - "jni", "moka", - "ndk-context", "once_cell", "parking_lot", - "rand 0.10.1", + "rand 0.9.4", "resolv-conf", "smallvec", - "system-configuration 0.7.0", "thiserror 2.0.18", "tokio", "tracing", @@ -2173,55 +2118,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn 2.0.117", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.117", -] - [[package]] name = "jobserver" version = "0.1.34" @@ -2496,12 +2392,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - [[package]] name = "nibble_vec" version = "0.1.0" @@ -2970,17 +2860,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prefix-trie" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" -dependencies = [ - "either", - "ipnet", - "num-traits", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -3216,17 +3095,6 @@ dependencies = [ "rand_core 0.9.5", ] -[[package]] -name = "rand" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -3265,12 +3133,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - [[package]] name = "rayon" version = "1.11.0" @@ -3396,7 +3258,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration 0.5.1", + "system-configuration", "tokio", "tokio-rustls 0.24.1", "tower-service", @@ -4229,7 +4091,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -4240,7 +4102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -4301,16 +4163,6 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" -[[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - [[package]] name = "simdutf8" version = "0.1.5" @@ -4697,18 +4549,7 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", - "system-configuration-sys 0.5.0", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags 2.11.0", - "core-foundation", - "system-configuration-sys 0.6.0", + "system-configuration-sys", ] [[package]] @@ -4721,16 +4562,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tagptr" version = "0.2.0" diff --git a/ENV.md b/ENV.md index 4d48f6627..2d3dc8b7c 100644 --- a/ENV.md +++ b/ENV.md @@ -40,6 +40,12 @@ These variables are for local repo tooling and development workflows. They are n | `SERVERBEE_LOG__LEVEL` | `log.level` | string | `info` | Log level: `trace`, `debug`, `info`, `warn`, `error` | | `SERVERBEE_LOG__FILE` | `log.file` | string | `""` | Log file path. Empty means stdout only | +### Development Only + +| Environment Variable | TOML Key | Type | Default | Description | +|---------------------|----------|------|---------|-------------| +| `SERVERBEE_DEV__DEMO_DATA` | `dev.demo_data` | bool | `false` | Reset and seed the local synthetic demo dataset. Only allowed when `database.path = "dev-demo.db"`; creates `admin` / `admin123` and in-memory demo agents for local development | + ### OAuth (Optional) | Environment Variable | TOML Key | Type | Default | Description | diff --git a/Makefile b/Makefile index 66948ed3e..7d172096c 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,8 @@ COMMAND_TARGETS := \ server-dev-prod \ agent-dev \ dev-full \ + dev-demo \ + server-dev-demo \ docker-build \ docker-up \ docker-down \ diff --git a/apps/docs/content/docs/cn/configuration.mdx b/apps/docs/content/docs/cn/configuration.mdx index 65f0a6dc4..da4e89c4a 100644 --- a/apps/docs/content/docs/cn/configuration.mdx +++ b/apps/docs/content/docs/cn/configuration.mdx @@ -61,6 +61,12 @@ ServerBee 使用 [figment](https://github.com/SergioBenitez/Figment) 库加载 | `SERVERBEE_LOG__LEVEL` | `info` | 日志级别:`trace`/`debug`/`info`/`warn`/`error` | | `SERVERBEE_LOG__FILE` | `""` | 日志文件路径,留空输出到 stdout | +#### 仅本地开发 + +| 环境变量 | 默认值 | 说明 | +|----------|--------|------| +| `SERVERBEE_DEV__DEMO_DATA` | `false` | 重置并写入本地合成 demo 数据集。仅允许与 `SERVERBEE_DATABASE__PATH=dev-demo.db` 一起使用;会创建 `admin` / `admin123` 并启动内存中的 demo agents | + #### OAuth(按需配置) | 环境变量 | 默认值 | 说明 | @@ -266,6 +272,12 @@ session_ttl = 86400 # 默认: 0 max_servers = 0 +# --- 本地开发 --- +[dev] +# 重置并写入合成 demo 数据。仅允许 database.path = "dev-demo.db"。 +# 默认: false +demo_data = false + # --- 速率限制 --- [rate_limit] # 登录接口速率限制:每 IP 每分钟最大请求数 diff --git a/apps/docs/content/docs/en/configuration.mdx b/apps/docs/content/docs/en/configuration.mdx index 1f1fc95ae..514ca0450 100644 --- a/apps/docs/content/docs/en/configuration.mdx +++ b/apps/docs/content/docs/en/configuration.mdx @@ -55,6 +55,12 @@ There is no admin username/password environment variable. On first start (when n | `SERVERBEE_LOG__LEVEL` | `info` | Log level: `trace`, `debug`, `info`, `warn`, `error` | | `SERVERBEE_LOG__FILE` | `""` | Log file path. Empty means stdout only | +#### Development Only + +| Environment Variable | Default | Description | +|---------------------|---------|-------------| +| `SERVERBEE_DEV__DEMO_DATA` | `false` | Reset and seed the local synthetic demo dataset. Only allowed with `SERVERBEE_DATABASE__PATH=dev-demo.db`; creates `admin` / `admin123` and in-memory demo agents for local development | + #### OAuth (Optional) | Environment Variable | Default | Description | @@ -239,6 +245,12 @@ Tunes the agent-side security event detectors (SSH login / brute force, port sca | `max_servers` | u32 | `0` | Maximum servers allowed via enrollment (0 = no limit). Best-effort soft cap | | `secure_cookie` | bool | `true` | Set the `Secure` flag on session cookies. Use `false` only when the browser accesses ServerBee over plain HTTP | +### `[dev]` -- Local Development + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `demo_data` | bool | `false` | Reset and seed the synthetic local development dataset. Guarded to `database.path = "dev-demo.db"` so it cannot accidentally run against normal local or production-copy databases | + ### `[retention]` -- Data Retention | Key | Type | Default | Description | diff --git a/apps/web/openapi.json b/apps/web/openapi.json index 86896a101..a73deb9ba 100644 --- a/apps/web/openapi.json +++ b/apps/web/openapi.json @@ -11740,13 +11740,22 @@ "cpu", "mem_used", "mem_total", + "swap_used", + "swap_total", "disk_used", "disk_total", + "disk_read_bytes_per_sec", + "disk_write_bytes_per_sec", "net_in_speed", "net_out_speed", + "net_in_transfer", + "net_out_transfer", "load_1", "load_5", "load_15", + "tcp_conn", + "udp_conn", + "process_count", "uptime" ], "properties": { @@ -11754,6 +11763,11 @@ "type": "number", "format": "double" }, + "disk_read_bytes_per_sec": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, "disk_total": { "type": "integer", "format": "int64", @@ -11764,6 +11778,11 @@ "format": "int64", "minimum": 0 }, + "disk_write_bytes_per_sec": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, "load_1": { "type": "number", "format": "double" @@ -11791,11 +11810,46 @@ "format": "int64", "minimum": 0 }, + "net_in_transfer": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, "net_out_speed": { "type": "integer", "format": "int64", "minimum": 0 }, + "net_out_transfer": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "process_count": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "swap_total": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "swap_used": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "tcp_conn": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "udp_conn": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, "uptime": { "type": "integer", "format": "int64", @@ -15800,4 +15854,4 @@ "description": "ASN MMDB database management for traceroute enrichment (DB-IP Lite ASN)" } ] -} \ No newline at end of file +} diff --git a/apps/web/src/components/status/layout-toggle.tsx b/apps/web/src/components/status/layout-toggle.tsx index f3f59b4ad..0bbfdd303 100644 --- a/apps/web/src/components/status/layout-toggle.tsx +++ b/apps/web/src/components/status/layout-toggle.tsx @@ -1,7 +1,6 @@ -import { LayoutGrid, List } from 'lucide-react' +import { LayoutGrid, Table2 } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { Button } from '@/components/ui/button' -import { cn } from '@/lib/utils' +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' interface Props { onChange: (next: 'list' | 'grid') => void @@ -11,25 +10,23 @@ interface Props { export function LayoutToggle({ value, onChange }: Props) { const { t } = useTranslation('status') return ( -
- - -
+ + ) } diff --git a/apps/web/src/components/status/server-summary-card.tsx b/apps/web/src/components/status/server-summary-card.tsx index 2d3c7eaa2..080eff4da 100644 --- a/apps/web/src/components/status/server-summary-card.tsx +++ b/apps/web/src/components/status/server-summary-card.tsx @@ -2,21 +2,70 @@ import { Link } from '@tanstack/react-router' import { Wrench } from 'lucide-react' import type { ReactNode } from 'react' import { useTranslation } from 'react-i18next' +import { CompactMetric } from '@/components/server/compact-metric' import { StatusBadge } from '@/components/server/status-badge' import { Badge } from '@/components/ui/badge' +import { RingChart } from '@/components/ui/ring-chart' import type { PublicServerSummary } from '@/lib/api-schema' -import { cn, countryCodeToFlag, formatSpeed, formatUptime } from '@/lib/utils' +import { computeTrafficQuota } from '@/lib/traffic' +import { cn, countryCodeToFlag, formatBytes, formatSpeed, formatUptime } from '@/lib/utils' -function ProgressBar({ value, label, color }: { color: string; label: string; value: number }) { - const pct = Math.min(100, Math.max(0, value)) +function clampPercent(value: number): number { + if (!Number.isFinite(value)) { + return 0 + } + return Math.min(100, Math.max(0, value)) +} + +function getRingColor(pct: number, brandColor: string): string { + if (pct > 90) { + return '#ef4444' + } + if (pct > 70) { + return '#f59e0b' + } + return brandColor +} + +function metricPercent(used: number, total: number): number { + return total > 0 ? (used / total) * 100 : 0 +} + +function finiteMetric(value: number | null | undefined): number { + return typeof value === 'number' && Number.isFinite(value) ? value : 0 +} + +function renderSpeedValue(bytesPerSec: number): ReactNode { + if (bytesPerSec <= 0) { + return '0' + } + const formatted = formatSpeed(bytesPerSec) + const lastSpace = formatted.lastIndexOf(' ') + if (lastSpace < 0) { + return formatted + } return ( -
-
- {label} - {pct.toFixed(1)}% -
-
-
+ <> + {formatted.slice(0, lastSpace)} + {formatted.slice(lastSpace + 1)} + + ) +} + +interface RingMetricProps { + color: string + label: string + subText: ReactNode + value: number +} + +function RingMetric({ color, label, subText, value }: RingMetricProps) { + return ( +
+ +
+ {label} + {subText}
) @@ -28,24 +77,47 @@ interface Props { } export function ServerSummaryCard({ server, clickable }: Props) { - const { t } = useTranslation('status') + const { t } = useTranslation(['status', 'servers']) const m = server.metrics - const memPct = m && m.mem_total > 0 ? (m.mem_used / m.mem_total) * 100 : 0 - const diskPct = m && m.disk_total > 0 ? (m.disk_used / m.disk_total) * 100 : 0 + const memPct = m ? metricPercent(m.mem_used, m.mem_total) : 0 + const diskPct = m ? metricPercent(m.disk_used, m.disk_total) : 0 + const swapPct = m ? metricPercent(m.swap_used, m.swap_total) : 0 + const processCount = m ? finiteMetric(m.process_count) : 0 + const tcpConn = m ? finiteMetric(m.tcp_conn) : 0 + const udpConn = m ? finiteMetric(m.udp_conn) : 0 + const traffic = m + ? computeTrafficQuota({ + entry: undefined, + netInTransfer: m.net_in_transfer, + netOutTransfer: m.net_out_transfer + }) + : { limit: 0, pct: 0, used: 0 } const flag = countryCodeToFlag(server.country_code) + const status = server.online ? 'online' : 'offline' const body: ReactNode = (
-
+ {!server.online && ( +