Compare commits
47 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 150d83be42 | |||
| 0ad49775fc | |||
| cc54502a12 | |||
| 80bf20cda1 | |||
| 88138341ae | |||
| 436eac2600 | |||
| c39fed294b | |||
| 441687f67d | |||
| e7e4509016 | |||
| c5f272b70e | |||
| 17662107b0 | |||
| d8b4843415 | |||
| 99d4b94e5c | |||
| 64f5115d54 | |||
| 975c4b1eaf | |||
| d63c701867 | |||
| c48a37d56a | |||
| 00cb8f7a9b | |||
| 70a70af859 | |||
| 512f597a0f | |||
| 389f2b886b | |||
| 3d69f530d4 | |||
| 3929a4a277 | |||
| 6109785e63 | |||
| 60e4fa3b9d | |||
| 000e31a14f | |||
| df1d1dddc6 | |||
| 4e17ef1e09 | |||
| 74464f6a48 | |||
| 142f76684b | |||
| d4e3ce6e27 | |||
| fba0d17403 | |||
| a8467a3cf1 | |||
| 78d0e553af | |||
| 89bf217afd | |||
| 349e0cf3ff | |||
| 4eb4c64398 | |||
| 12e3a30325 | |||
| 97b16624b6 | |||
| 4347996e2b | |||
| d80bc544dc | |||
| 60a1a87d04 | |||
| 2a6b233909 | |||
| d3ff4d3916 | |||
| 5a77198344 | |||
| 6e94081db2 | |||
| 3c95626cf9 |
22 changed files with 1249 additions and 588 deletions
180
.forgejo/workflows/pre-release.yaml
Normal file
180
.forgejo/workflows/pre-release.yaml
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
name: Multi-Platform Release Build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*.*.*-*"
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: docker
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: native
|
||||||
|
cross: false
|
||||||
|
platform: linux/arm64
|
||||||
|
|
||||||
|
- target: x86_64-unknown-linux-gnu
|
||||||
|
cross: true
|
||||||
|
platform: linux/amd64
|
||||||
|
deps: "build-essential gcc-x86-64-linux-gnu libc6-dev-amd64-cross pkg-config binutils-x86-64-linux-gnu"
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: x86_64-linux-gnu-gcc
|
||||||
|
|
||||||
|
- target: x86_64-pc-windows-gnu
|
||||||
|
cross: true
|
||||||
|
platform: linux/amd64
|
||||||
|
deps: "gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm"
|
||||||
|
env:
|
||||||
|
CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc
|
||||||
|
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: https://github.com/dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: https://code.forgejo.org/actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-${{ matrix.target }}-
|
||||||
|
${{ runner.os }}-cargo-
|
||||||
|
|
||||||
|
- name: Native Build Release
|
||||||
|
if: matrix.target == 'native'
|
||||||
|
run: |
|
||||||
|
NATIVE_TARGET=$(rustc -vV | grep 'host:' | cut -d' ' -f2)
|
||||||
|
echo "Building for native target: $NATIVE_TARGET"
|
||||||
|
cargo build --release
|
||||||
|
TARGET_BINARY="target/release/uptime-kuma-dashboard"
|
||||||
|
OUTPUT_NAME="uptime-kuma-dashboard-$NATIVE_TARGET"
|
||||||
|
mkdir -p release-artifacts
|
||||||
|
cp "$TARGET_BINARY" "release-artifacts/$OUTPUT_NAME"
|
||||||
|
strip "release-artifacts/$OUTPUT_NAME" 2>/dev/null || true
|
||||||
|
chmod +x "release-artifacts/$OUTPUT_NAME"
|
||||||
|
ls -lh "release-artifacts/$OUTPUT_NAME"
|
||||||
|
|
||||||
|
- name: Get Rust version
|
||||||
|
id: rust-version
|
||||||
|
run: echo "version=$(rustc --version | cut -d' ' -f2)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cross-Build Linux x86_64 Release
|
||||||
|
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
||||||
|
uses: docker://rust:slim
|
||||||
|
with:
|
||||||
|
args: >
|
||||||
|
sh -c "
|
||||||
|
rustup target add ${{ matrix.target }} &&
|
||||||
|
apt-get update && apt-get install -y ${{ matrix.deps }} &&
|
||||||
|
mkdir -p ~/.cargo &&
|
||||||
|
echo '[target.${{ matrix.target }}]\nlinker = \"x86_64-linux-gnu-gcc\"' > ~/.cargo/config.toml &&
|
||||||
|
cargo build --release --target ${{ matrix.target }} &&
|
||||||
|
mkdir -p release-artifacts &&
|
||||||
|
cp target/${{ matrix.target }}/release/uptime-kuma-dashboard release-artifacts/uptime-kuma-dashboard-${{ matrix.target }} &&
|
||||||
|
x86_64-linux-gnu-strip release-artifacts/uptime-kuma-dashboard-${{ matrix.target }} &&
|
||||||
|
chmod +x release-artifacts/uptime-kuma-dashboard-${{ matrix.target }}"
|
||||||
|
env: ${{ matrix.env }}
|
||||||
|
options: --platform ${{ matrix.platform }}
|
||||||
|
|
||||||
|
- name: Cross-Build Windows x86_64 Release
|
||||||
|
if: matrix.target == 'x86_64-pc-windows-gnu'
|
||||||
|
uses: docker://rust:slim
|
||||||
|
with:
|
||||||
|
args: >
|
||||||
|
sh -c "
|
||||||
|
rustup target add ${{ matrix.target }} &&
|
||||||
|
apt-get update && apt-get install -y ${{ matrix.deps }} &&
|
||||||
|
cargo build --release --target ${{ matrix.target }} &&
|
||||||
|
mkdir -p release-artifacts &&
|
||||||
|
cp target/${{ matrix.target }}/release/uptime-kuma-dashboard.exe release-artifacts/uptime-kuma-dashboard-${{ matrix.target }}.exe &&
|
||||||
|
ls -lh release-artifacts/"
|
||||||
|
env: ${{ matrix.env }}
|
||||||
|
options: --platform ${{ matrix.platform }}
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: https://data.forgejo.org/forgejo/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: binary-${{ matrix.target }}
|
||||||
|
path: release-artifacts/
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
create-release:
|
||||||
|
runs-on: docker
|
||||||
|
needs: build
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: https://data.forgejo.org/forgejo/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: all-artifacts/
|
||||||
|
|
||||||
|
- name: Prepare release artifacts
|
||||||
|
run: |
|
||||||
|
mkdir -p release-artifacts
|
||||||
|
find all-artifacts -type f -name "uptime-kuma-dashboard-*" -exec cp {} release-artifacts/ \;
|
||||||
|
ls -lh release-artifacts/
|
||||||
|
|
||||||
|
- name: Generate checksums
|
||||||
|
run: |
|
||||||
|
cd release-artifacts
|
||||||
|
sha256sum uptime-kuma-dashboard-* > SHA256SUMS.txt
|
||||||
|
cat SHA256SUMS.txt
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: https://code.forgejo.org/actions/forgejo-release@v2
|
||||||
|
with:
|
||||||
|
direction: upload
|
||||||
|
url: https://git.marcodearaujo.com
|
||||||
|
repo: marcodearaujo/uptime-kuma-dashboard
|
||||||
|
token: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
release-dir: release-artifacts
|
||||||
|
title: "Pre-Release ${{ forgejo.ref_name }}"
|
||||||
|
prerelease: true
|
||||||
|
tag: ${{ forgejo.ref_name }}
|
||||||
|
release-notes: |
|
||||||
|
## 🚀 Multi-Platform Release
|
||||||
|
Compiled on Forgejo Runner **${{ runner.arch }}**
|
||||||
|
|
||||||
|
### 📦 Available Binaries
|
||||||
|
This release includes binaries for the following platforms:
|
||||||
|
- **Linux x86_64** (Intel/AMD 64-bit)
|
||||||
|
- **Linux ARM64** (aarch64) - Raspberry Pi 4, ARM servers
|
||||||
|
- **Windows x86_64** (64-bit)
|
||||||
|
|
||||||
|
### 🚀 How to use
|
||||||
|
#### Linux/ARM:
|
||||||
|
# Download the appropriate binary
|
||||||
|
wget https://your-forgejo.com/releases/download/${{ forgejo.ref_name }}/uptime-kuma-dashboard-<your-target>
|
||||||
|
# Make executable
|
||||||
|
chmod +x uptime-kuma-dashboard-*
|
||||||
|
# Run
|
||||||
|
./uptime-kuma-dashboard-* --base-url https://your-kuma --slug your-slug
|
||||||
|
|
||||||
|
#### Windows:
|
||||||
|
# Download uptime-kuma-dashboard-x86_64-pc-windows-gnu.exe
|
||||||
|
# Run in PowerShell or CMD
|
||||||
|
.\uptime-kuma-dashboard-x86_64-pc-windows-gnu.exe --base-url https://your-kuma --slug your-slug
|
||||||
|
|
||||||
|
### ✅ Verify Checksums
|
||||||
|
sha256sum -c SHA256SUMS.txt
|
||||||
|
|
||||||
|
### 🏗️ Build Information
|
||||||
|
- Rust Version: ${{ steps.rust-version.outputs.version }}
|
||||||
|
- Build Date: $(date +'%Y-%m-%d')
|
||||||
|
- Build Type: Release (optimized with LTO)
|
||||||
|
|
@ -17,18 +17,14 @@ jobs:
|
||||||
- target: x86_64-unknown-linux-gnu
|
- target: x86_64-unknown-linux-gnu
|
||||||
cross: true
|
cross: true
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
deps: "build-essential libssl-dev pkg-config gcc-x86_64-linux-gnu"
|
deps: "build-essential gcc-x86-64-linux-gnu libc6-dev-amd64-cross pkg-config binutils-x86-64-linux-gnu"
|
||||||
env:
|
env:
|
||||||
PKG_CONFIG_ALLOW_CROSS: "1"
|
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: x86_64-linux-gnu-gcc
|
||||||
OPENSSL_STATIC: "1"
|
|
||||||
OPENSSL_LIB_DIR: "/usr/lib/x86_64-linux-gnu"
|
|
||||||
OPENSSL_INCLUDE_DIR: "/usr/include/openssl"
|
|
||||||
PKG_CONFIG_ALL_STATIC: "1"
|
|
||||||
|
|
||||||
- target: x86_64-pc-windows-gnu
|
- target: x86_64-pc-windows-gnu
|
||||||
cross: true
|
cross: true
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
deps: "build-essential gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64"
|
deps: "gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm"
|
||||||
env:
|
env:
|
||||||
CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc
|
CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc
|
||||||
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
|
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
|
||||||
|
|
@ -42,10 +38,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
|
||||||
- name: Add Rust target
|
|
||||||
if: matrix.target != 'native' && !matrix.container
|
|
||||||
run: rustup target add ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Cache Rust dependencies
|
- name: Cache Rust dependencies
|
||||||
uses: https://code.forgejo.org/actions/cache@v4
|
uses: https://code.forgejo.org/actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -74,21 +66,25 @@ jobs:
|
||||||
chmod +x "release-artifacts/$OUTPUT_NAME"
|
chmod +x "release-artifacts/$OUTPUT_NAME"
|
||||||
ls -lh "release-artifacts/$OUTPUT_NAME"
|
ls -lh "release-artifacts/$OUTPUT_NAME"
|
||||||
|
|
||||||
|
- name: Get Rust version
|
||||||
|
id: rust-version
|
||||||
|
run: echo "version=$(rustc --version | cut -d' ' -f2)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Cross-Build Linux x86_64 Release
|
- name: Cross-Build Linux x86_64 Release
|
||||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
||||||
uses: docker://rust:slim
|
uses: docker://rust:slim
|
||||||
with:
|
with:
|
||||||
args: >
|
args: >
|
||||||
sh -c "
|
sh -c "
|
||||||
echo 'OPENSSL_DIR:' $OPENSSL_DIR &&
|
rustup target add ${{ matrix.target }} &&
|
||||||
apt-get update && apt-get install -y ${{ matrix.deps }} &&
|
apt-get update && apt-get install -y ${{ matrix.deps }} &&
|
||||||
rustup target add x86_64-unknown-linux-gnu &&
|
mkdir -p ~/.cargo &&
|
||||||
cargo build --release --target x86_64-unknown-linux-gnu &&
|
echo '[target.${{ matrix.target }}]\nlinker = \"x86_64-linux-gnu-gcc\"' > ~/.cargo/config.toml &&
|
||||||
|
cargo build --release --target ${{ matrix.target }} &&
|
||||||
mkdir -p release-artifacts &&
|
mkdir -p release-artifacts &&
|
||||||
cp target/x86_64-unknown-linux-gnu/release/uptime-kuma-dashboard release-artifacts/uptime-kuma-dashboard-x86_64-unknown-linux-gnu &&
|
cp target/${{ matrix.target }}/release/uptime-kuma-dashboard release-artifacts/uptime-kuma-dashboard-${{ matrix.target }} &&
|
||||||
strip release-artifacts/uptime-kuma-dashboard-x86_64-unknown-linux-gnu &&
|
x86_64-linux-gnu-strip release-artifacts/uptime-kuma-dashboard-${{ matrix.target }} &&
|
||||||
chmod +x release-artifacts/uptime-kuma-dashboard-x86_64-unknown-linux-gnu &&
|
chmod +x release-artifacts/uptime-kuma-dashboard-${{ matrix.target }}"
|
||||||
ls -lh release-artifacts/"
|
|
||||||
env: ${{ matrix.env }}
|
env: ${{ matrix.env }}
|
||||||
options: --platform ${{ matrix.platform }}
|
options: --platform ${{ matrix.platform }}
|
||||||
|
|
||||||
|
|
@ -98,11 +94,11 @@ jobs:
|
||||||
with:
|
with:
|
||||||
args: >
|
args: >
|
||||||
sh -c "
|
sh -c "
|
||||||
|
rustup target add ${{ matrix.target }} &&
|
||||||
apt-get update && apt-get install -y ${{ matrix.deps }} &&
|
apt-get update && apt-get install -y ${{ matrix.deps }} &&
|
||||||
rustup target add x86_64-pc-windows-gnu &&
|
cargo build --release --target ${{ matrix.target }} &&
|
||||||
cargo build --release --target x86_64-pc-windows-gnu &&
|
|
||||||
mkdir -p release-artifacts &&
|
mkdir -p release-artifacts &&
|
||||||
cp target/x86_64-pc-windows-gnu/release/uptime-kuma-dashboard.exe release-artifacts/uptime-kuma-dashboard-x86_64-pc-windows-gnu.exe &&
|
cp target/${{ matrix.target }}/release/uptime-kuma-dashboard.exe release-artifacts/uptime-kuma-dashboard-${{ matrix.target }}.exe &&
|
||||||
ls -lh release-artifacts/"
|
ls -lh release-artifacts/"
|
||||||
env: ${{ matrix.env }}
|
env: ${{ matrix.env }}
|
||||||
options: --platform ${{ matrix.platform }}
|
options: --platform ${{ matrix.platform }}
|
||||||
|
|
@ -142,42 +138,42 @@ jobs:
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: https://code.forgejo.org/actions/forgejo-release@v2
|
uses: https://code.forgejo.org/actions/forgejo-release@v2
|
||||||
env:
|
|
||||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
|
||||||
with:
|
with:
|
||||||
direction: upload
|
direction: upload
|
||||||
|
url: https://git.marcodearaujo.com
|
||||||
|
repo: marcodearaujo/uptime-kuma-dashboard
|
||||||
|
token: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
release-dir: release-artifacts
|
release-dir: release-artifacts
|
||||||
title: "Release ${{ forgejo.ref_name }}"
|
title: "Release ${{ forgejo.ref_name }}"
|
||||||
tag: ${{ forgejo.ref_name }}
|
tag: ${{ forgejo.ref_name }}
|
||||||
release-notes:: |
|
release-notes: |
|
||||||
## 🚀 Multi-Platform Release
|
## 🚀 Multi-Platform Release
|
||||||
Compiled on runner **${{ env.RUNNER_ARCH }}**
|
Compiled on Forgejo Runner **${{ runner.arch }}**
|
||||||
|
|
||||||
### 📦 Available Binaries
|
### 📦 Available Binaries
|
||||||
This release includes binaries for the following platforms:
|
This release includes binaries for the following platforms:
|
||||||
- **Linux x86_64** (Intel/AMD 64-bit)
|
- **Linux x86_64** (Intel/AMD 64-bit)
|
||||||
- **Linux ARM64** (aarch64) - Raspberry Pi 4, ARM servers
|
- **Linux ARM64** (aarch64) - Raspberry Pi 4, ARM servers
|
||||||
- **Windows x86_64** (64-bit)
|
- **Windows x86_64** (64-bit)
|
||||||
|
|
||||||
### 🚀 How to use
|
### 🚀 How to use
|
||||||
#### Linux/ARM:
|
#### Linux/ARM:
|
||||||
```bash
|
|
||||||
# Download the appropriate binary
|
# Download the appropriate binary
|
||||||
wget https://your-forgejo.com/releases/download/${{ forgejo.ref_name }}/uptime-kuma-dashboard-<your-target>
|
wget https://your-forgejo.com/releases/download/${{ forgejo.ref_name }}/uptime-kuma-dashboard-<your-target>
|
||||||
# Make executable
|
# Make executable
|
||||||
chmod +x uptime-kuma-dashboard-*
|
chmod +x uptime-kuma-dashboard-*
|
||||||
# Run
|
# Run
|
||||||
./uptime-kuma-dashboard-* --base-url https://your-kuma --slug your-slug
|
./uptime-kuma-dashboard-* --base-url https://your-kuma --slug your-slug
|
||||||
```
|
|
||||||
#### Windows:
|
#### Windows:
|
||||||
```powershell
|
|
||||||
# Download uptime-kuma-dashboard-x86_64-pc-windows-gnu.exe
|
# Download uptime-kuma-dashboard-x86_64-pc-windows-gnu.exe
|
||||||
# Run in PowerShell or CMD
|
# Run in PowerShell or CMD
|
||||||
.\uptime-kuma-dashboard-x86_64-pc-windows-gnu.exe --base-url https://your-kuma --slug your-slug
|
.\uptime-kuma-dashboard-x86_64-pc-windows-gnu.exe --base-url https://your-kuma --slug your-slug
|
||||||
```
|
|
||||||
### ✅ Verify Checksums
|
### ✅ Verify Checksums
|
||||||
```bash
|
|
||||||
sha256sum -c SHA256SUMS.txt
|
sha256sum -c SHA256SUMS.txt
|
||||||
```
|
|
||||||
### 🏗️ Build Information
|
### 🏗️ Build Information
|
||||||
- Runner Architecture: ${{ env.RUNNER_ARCH }}
|
- Rust Version: ${{ steps.rust-version.outputs.version }}
|
||||||
- Rust Version: 1.84.0
|
- Build Date: $(date +'%Y-%m-%d')
|
||||||
- Build Type: Release (optimized)
|
- Build Type: Release (optimized with LTO)
|
||||||
|
|
|
||||||
34
.vscode/launch.json
vendored
Normal file
34
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug executable 'uptime-kuma-dashboard'",
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"--bin=uptime-kuma-dashboard"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"args": [
|
||||||
|
"--base-url=http://192.168.69.10:3001/",
|
||||||
|
"--slug=formgen",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug unit tests in executable 'uptime-kuma-dashboard'",
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"test",
|
||||||
|
"--bin=uptime-kuma-dashboard"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
657
Cargo.lock
generated
657
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
19
Cargo.toml
19
Cargo.toml
|
|
@ -1,23 +1,28 @@
|
||||||
[package]
|
[package]
|
||||||
name = "uptime-kuma-dashboard"
|
name = "uptime-kuma-dashboard"
|
||||||
version = "0.1.0"
|
version = "0.38.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5", features = ["derive", "env"] }
|
clap = { version = "4.5.57", features = ["derive", "env"] }
|
||||||
reqwest = { version = "0.12.28", features = ["blocking", "json"] }
|
reqwest = { version = "0.13.1", default-features = false, features = [
|
||||||
anyhow = "1.0"
|
"blocking",
|
||||||
|
"json",
|
||||||
|
"rustls",
|
||||||
|
] }
|
||||||
|
anyhow = "1.0.101"
|
||||||
fluent-templates = "0.13.2"
|
fluent-templates = "0.13.2"
|
||||||
unic-langid = "0.9.6"
|
unic-langid = "0.9.6"
|
||||||
sys-locale = "0.3.2"
|
sys-locale = "0.3.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0.148"
|
serde_json = "1.0.149"
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
url = "2.5.7"
|
url = "2.5.8"
|
||||||
ratatui = "0.30.0"
|
ratatui = "0.30.0"
|
||||||
crossterm = "0.29.0"
|
crossterm = "0.29.0"
|
||||||
chrono = "0.4.42"
|
chrono = "0.4.43"
|
||||||
rayon = "1.11.0"
|
rayon = "1.11.0"
|
||||||
|
rustls = { version = "0.23.36", default-features = false, features = ["ring"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Marco De Araujo
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
145
README.md
Normal file
145
README.md
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
# Uptime Kuma Terminal Dashboard
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
A terminal-based monitoring dashboard for Uptime Kuma that displays service status, uptime percentages, and historical performance data directly in your terminal. Built with Rust for performance and reliability.
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
<video width="800" controls>
|
||||||
|
<source src="assets/demo.webm" type="video/webm">
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This personal project started as a way to monitor my home lab services without needing a web browser. It connects to your Uptime Kuma instance API and presents a clean, color-coded view of your services' status with real-time updates. The dashboard works great on servers, remote terminals, or anywhere you prefer a lightweight monitoring solution.
|
||||||
|
|
||||||
|
Perfect for DevOps engineers, system administrators, or anyone who prefers terminal workflows over web interfaces for quick status checks.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Real-time service monitoring with automatic refresh
|
||||||
|
- Color-coded status indicators (green for up, red for down)
|
||||||
|
- Historical uptime visualization with compact status history lines
|
||||||
|
- Responsive terminal UI that adapts to window size
|
||||||
|
- Internationalization support (English and Portuguese)
|
||||||
|
- Configurable refresh intervals
|
||||||
|
- Minimal resource usage (runs efficiently even on low-power devices)
|
||||||
|
- Keyboard navigation support (arrow keys, home/end)
|
||||||
|
- Mobile-friendly design for SSH connections
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
This project demonstrates proficiency with several modern technologies:
|
||||||
|
|
||||||
|
- **Rust** (core language with async capabilities)
|
||||||
|
- **ratatui** (terminal UI framework)
|
||||||
|
- **reqwest** (HTTP client for API communication)
|
||||||
|
- **serde** (JSON serialization/deserialization)
|
||||||
|
- **clap** (command-line argument parsing)
|
||||||
|
- **fluent-templates** (internationalization framework)
|
||||||
|
- REST API integration patterns
|
||||||
|
- Error handling and logging best practices
|
||||||
|
- Cross-platform terminal development
|
||||||
|
|
||||||
|
You can find the latest releases in the [Releases section](https://git.marcodearaujo.com/marcodearaujo/uptime-kuma-dashboard/releases).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Option 1: Download built Binary (Recommended)
|
||||||
|
|
||||||
|
1. Go to the [Releases page](https://git.marcodearaujo.com/marcodearaujo/uptime-kuma-dashboard/releases)
|
||||||
|
2. Download the binary for your operating system:
|
||||||
|
- **Linux (x86_64)**: `uptime-kuma-dashboard-x86_64-unknown-linux-gnu`
|
||||||
|
- **Linux (ARM64)**: `uptime-kuma-dashboard-aarch64-unknown-linux-gnu` (Raspberry Pi, ARM servers)
|
||||||
|
- **Windows**: `uptime-kuma-dashboard-x86_64-pc-windows-gnu.exe`
|
||||||
|
3. Verify the checksum (recommended for security):
|
||||||
|
```bash
|
||||||
|
sha256sum -c SHA256SUMS.txt
|
||||||
|
```
|
||||||
|
4. Make it executable (Linux/ARM only):
|
||||||
|
```bash
|
||||||
|
chmod +x uptime-kuma-dashboard-*
|
||||||
|
```
|
||||||
|
5. Run with your parameters:
|
||||||
|
```bash
|
||||||
|
./uptime-kuma-dashboard-* --base-url="http://your-kuma:3001/" --slug="your-slug"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Build From Source
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Rust 1.74+ toolchain (install via [rustup](https://rustup.rs/))
|
||||||
|
- Uptime Kuma instance with API access
|
||||||
|
|
||||||
|
### Build from Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://git.marcodearaujo.com/marcodearaujo/uptime-kuma-dashboard.git
|
||||||
|
cd uptime-kuma-dashboard
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Run the dashboard (replace with your Uptime Kuma URL and status page slug)
|
||||||
|
./target/release/uptime-kuma-dashboard \
|
||||||
|
--base-url="http://your-uptime-kuma-instance:3001/" \
|
||||||
|
--slug="your-status-page-slug"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The dashboard requires two parameters:
|
||||||
|
|
||||||
|
- `--base-url`: Your Uptime Kuma instance URL (including port)
|
||||||
|
- `--slug`: Your status page slug identifier
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./uptime-kuma-dashboard --base-url="http://192.168.1.100:3001/" --slug="home-services"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Controls during runtime:**
|
||||||
|
|
||||||
|
- `q` or `ESC`: Exit the dashboard
|
||||||
|
- `↑`/`↓` or `k`/`j`: Navigate through monitors
|
||||||
|
- `Home`/`End`: Jump to top/bottom of the list
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
You can set environment variables to avoid typing parameters each time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export UPTIME_KUMA_URL="http://your-uptime-kuma-instance:3001/"
|
||||||
|
export STATUS_PAGE_SLUG="your-status-page-slug"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
This project follows Rust best practices including:
|
||||||
|
|
||||||
|
- Comprehensive error handling with `anyhow`
|
||||||
|
- Internationalization-ready architecture
|
||||||
|
- Modular code organization
|
||||||
|
- Performance optimization for terminal rendering
|
||||||
|
- Cross-platform compatibility testing
|
||||||
|
|
||||||
|
The architecture separates concerns into:
|
||||||
|
|
||||||
|
- API client layer
|
||||||
|
- Data processing core
|
||||||
|
- UI rendering components
|
||||||
|
- Configuration management
|
||||||
|
- Internationalization system
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is available under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Developed as a personal learning project to improve Rust skills and create a useful tool for my home lab monitoring. Feedback welcome!_
|
||||||
BIN
assets/demo.webm
Normal file
BIN
assets/demo.webm
Normal file
Binary file not shown.
|
|
@ -1,9 +1,11 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::endpoints::UptimeKumaEndpoints,
|
api::endpoints::UptimeKumaEndpoints,
|
||||||
data::{self, heartbeat::HeartbeatResponse, status_page::model::StatusPageResponse},
|
data::{self, heartbeat::HeartbeatResponse, status_page::model::StatusPageResponse}, i18n::t,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -13,29 +15,37 @@ pub struct UptimeKumaClient {
|
||||||
|
|
||||||
impl UptimeKumaClient {
|
impl UptimeKumaClient {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let client = Client::builder().connect_timeout(Duration::from_secs(10)).timeout(Duration::from_secs(30)).build().unwrap_or_else(|_| panic!("{}", t("http-build-error")));
|
||||||
Self {
|
Self {
|
||||||
client: Client::new(),
|
client,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_heartbeat(&self, endpoints: &UptimeKumaEndpoints) -> Result<HeartbeatResponse> {
|
pub fn fetch_heartbeat(&self, endpoints: &UptimeKumaEndpoints) -> Result<HeartbeatResponse> {
|
||||||
let response = self.client.get(endpoints.heartbeat_url()).send()?;
|
self.fetch_and_parse(endpoints.heartbeat_url(), data::heartbeat::parse_response)
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
let json_text = response.text()?;
|
|
||||||
data::heartbeat::parse_response(&json_text)
|
|
||||||
} else {
|
|
||||||
return Err(anyhow::anyhow!(response.status()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_status_page(&self, endpoints: &UptimeKumaEndpoints) -> Result<StatusPageResponse> {
|
pub fn fetch_status_page(&self, endpoints: &UptimeKumaEndpoints) -> Result<StatusPageResponse> {
|
||||||
let response = self.client.get(endpoints.status_page_url()).send()?;
|
self.fetch_and_parse(
|
||||||
|
endpoints.status_page_url(),
|
||||||
|
data::status_page::parse_response,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_and_parse<T, F>(&self, url: String, parser: F) -> Result<T>
|
||||||
|
where
|
||||||
|
F: FnOnce(&str) -> Result<T>,
|
||||||
|
{
|
||||||
|
let response = self.client.get(url.clone()).send()?;
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
let json_text = response.text()?;
|
let json_text = response.text()?;
|
||||||
data::status_page::parse_response(&json_text)
|
parser(&json_text)
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow::anyhow!(response.status()));
|
Err(anyhow::anyhow!(
|
||||||
|
"URL: {}, Error: {}",
|
||||||
|
url,
|
||||||
|
response.status()
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,28 @@ impl UptimeKumaEndpoints {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn heartbeat_url(&self) -> String {
|
pub fn heartbeat_url(&self) -> String {
|
||||||
format!("{}api/status-page/heartbeat/{}", self.base_url, self.slug)
|
let mut url = self.get_url();
|
||||||
|
url.path_segments_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push("heartbeat")
|
||||||
|
.push(&self.slug);
|
||||||
|
url.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status_page_url(&self) -> String {
|
pub fn status_page_url(&self) -> String {
|
||||||
format!("{}api/status-page/{}", self.base_url, self.slug)
|
let mut url = self.get_url();
|
||||||
|
url.path_segments_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push(&self.slug);
|
||||||
|
url.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_url(&self) -> Url {
|
||||||
|
let mut url = self.base_url.clone();
|
||||||
|
url.path_segments_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push("api")
|
||||||
|
.push("status-page");
|
||||||
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::core::models::{UnifiedData, UnifiedGroupData, UnifiedMonitorData};
|
use crate::core::models::{UnifiedData, UnifiedGroupData, UnifiedMonitorData};
|
||||||
|
|
@ -6,32 +7,45 @@ use crate::data::{
|
||||||
status_page::model::StatusPageResponse,
|
status_page::model::StatusPageResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn unify_data(status_page: &StatusPageResponse, heartbeat: &HeartbeatResponse) -> UnifiedData {
|
pub fn unify_data<'a>(
|
||||||
|
status_page: &'a StatusPageResponse,
|
||||||
|
heartbeat: &'a HeartbeatResponse,
|
||||||
|
) -> UnifiedData<'a> {
|
||||||
let mut groups = Vec::with_capacity(status_page.public_group_list.len());
|
let mut groups = Vec::with_capacity(status_page.public_group_list.len());
|
||||||
let heartbeat_map: HashMap<u64, Vec<HeartbeatEntry>> = heartbeat
|
|
||||||
|
let mut heartbeat_map: HashMap<u64, &'a [HeartbeatEntry]> =
|
||||||
|
HashMap::with_capacity(heartbeat.monitors.len());
|
||||||
|
heartbeat_map.extend(
|
||||||
|
heartbeat
|
||||||
.monitors
|
.monitors
|
||||||
.iter()
|
.iter()
|
||||||
.map(|m| (m.monitor_id, m.heartbeats.clone()))
|
.map(|m| (m.monitor_id, &m.heartbeats[..])),
|
||||||
.collect();
|
);
|
||||||
|
|
||||||
let uptime_map: HashMap<(u64, u32), UptimeData> = heartbeat
|
let mut uptime_map: HashMap<(u64, u32), &'a UptimeData> =
|
||||||
|
HashMap::with_capacity(heartbeat.uptime_data.len());
|
||||||
|
uptime_map.extend(
|
||||||
|
heartbeat
|
||||||
.uptime_data
|
.uptime_data
|
||||||
.iter()
|
.iter()
|
||||||
.map(|u| ((u.monitor_id, u.period_hours), u.clone()))
|
.map(|u| ((u.monitor_id, u.period_hours), u)),
|
||||||
.collect();
|
);
|
||||||
|
|
||||||
for group in &status_page.public_group_list {
|
for group in &status_page.public_group_list {
|
||||||
let mut monitors = Vec::with_capacity(group.monitor_list.len());
|
let mut monitors = Vec::with_capacity(group.monitor_list.len());
|
||||||
for monitor_info in &group.monitor_list {
|
for monitor_info in &group.monitor_list {
|
||||||
let uptime_data = uptime_map.get(&(monitor_info.id, 24)).cloned();
|
let uptime_data = uptime_map.get(&(monitor_info.id, 24)).copied();
|
||||||
let heartbeats = heartbeat_map
|
let heartbeats = heartbeat_map.get(&monitor_info.id).copied().unwrap_or(&[]);
|
||||||
.get(&monitor_info.id)
|
|
||||||
.cloned()
|
let name: Cow<'a, str> = if monitor_info.name.len() > 100 {
|
||||||
.unwrap_or_default();
|
Cow::Owned(monitor_info.name.clone())
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(monitor_info.name.as_str())
|
||||||
|
};
|
||||||
|
|
||||||
monitors.push(UnifiedMonitorData {
|
monitors.push(UnifiedMonitorData {
|
||||||
id: monitor_info.id,
|
id: monitor_info.id,
|
||||||
name: monitor_info.name.clone(),
|
name,
|
||||||
heartbeats,
|
heartbeats,
|
||||||
uptime_data,
|
uptime_data,
|
||||||
});
|
});
|
||||||
|
|
@ -40,7 +54,7 @@ pub fn unify_data(status_page: &StatusPageResponse, heartbeat: &HeartbeatRespons
|
||||||
monitors.sort_by_key(|m| m.id);
|
monitors.sort_by_key(|m| m.id);
|
||||||
|
|
||||||
groups.push(UnifiedGroupData {
|
groups.push(UnifiedGroupData {
|
||||||
group_info: group.clone(),
|
group_info: group,
|
||||||
monitors,
|
monitors,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -48,8 +62,8 @@ pub fn unify_data(status_page: &StatusPageResponse, heartbeat: &HeartbeatRespons
|
||||||
groups.sort_by_key(|g| g.group_info.weight);
|
groups.sort_by_key(|g| g.group_info.weight);
|
||||||
|
|
||||||
UnifiedData {
|
UnifiedData {
|
||||||
title: status_page.config.title.clone(),
|
title: Cow::Borrowed(&status_page.config.title),
|
||||||
description: status_page.config.description.clone(),
|
description: status_page.config.description.as_deref().map(Cow::Borrowed),
|
||||||
groups,
|
groups,
|
||||||
auto_refresh_interval: status_page.config.auto_refresh_interval,
|
auto_refresh_interval: status_page.config.auto_refresh_interval,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,26 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::data::heartbeat::model::{HeartbeatEntry, UptimeData};
|
use crate::data::heartbeat::model::{HeartbeatEntry, UptimeData};
|
||||||
use crate::data::status_page::model::{StatusPageGroup};
|
use crate::data::status_page::model::{StatusPageGroup};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct UnifiedMonitorData {
|
pub struct UnifiedMonitorData<'a> {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub name: String,
|
pub name: Cow<'a, str>,
|
||||||
pub heartbeats: Vec<HeartbeatEntry>,
|
pub heartbeats: &'a [HeartbeatEntry],
|
||||||
pub uptime_data: Option<UptimeData>,
|
pub uptime_data: Option<&'a UptimeData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct UnifiedData {
|
pub struct UnifiedData<'a> {
|
||||||
pub title: String,
|
pub title: Cow<'a, str>,
|
||||||
pub description: Option<String>,
|
pub description: Option<Cow<'a, str>>,
|
||||||
pub auto_refresh_interval: u32,
|
pub auto_refresh_interval: u32,
|
||||||
pub groups: Vec<UnifiedGroupData>,
|
pub groups: Vec<UnifiedGroupData<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct UnifiedGroupData {
|
pub struct UnifiedGroupData<'a> {
|
||||||
pub group_info: StatusPageGroup,
|
pub group_info: &'a StatusPageGroup,
|
||||||
pub monitors: Vec<UnifiedMonitorData>,
|
pub monitors: Vec<UnifiedMonitorData<'a>>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ pub struct HeartbeatEntry {
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub msg: String,
|
pub msg: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ping: Option<u64>,
|
pub ping: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
|
@ -55,7 +55,7 @@ impl HeartbeatResponse {
|
||||||
for (monitor_id_str, heartbeats) in &self.heartbeat_list_raw {
|
for (monitor_id_str, heartbeats) in &self.heartbeat_list_raw {
|
||||||
let monitor_id = monitor_id_str
|
let monitor_id = monitor_id_str
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.with_context(|| format!("{}", t("invalid-monitor-id")))?;
|
.with_context(|| t("invalid-monitor-id").to_string())?;
|
||||||
|
|
||||||
self.monitors.push(MonitorHeartbeats {
|
self.monitors.push(MonitorHeartbeats {
|
||||||
monitor_id,
|
monitor_id,
|
||||||
|
|
@ -77,11 +77,11 @@ impl HeartbeatResponse {
|
||||||
|
|
||||||
let monitor_id = parts[0]
|
let monitor_id = parts[0]
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.with_context(|| format!("{}", t("invalid-monitor-id")))?;
|
.with_context(|| t("invalid-monitor-id").to_string())?;
|
||||||
|
|
||||||
let period_hours = parts[1]
|
let period_hours = parts[1]
|
||||||
.parse::<u32>()
|
.parse::<u32>()
|
||||||
.with_context(|| format!("{}", t("invalid-period-hours")))?;
|
.with_context(|| t("invalid-period-hours").to_string())?;
|
||||||
|
|
||||||
self.uptime_data.push(UptimeData {
|
self.uptime_data.push(UptimeData {
|
||||||
monitor_id,
|
monitor_id,
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ use crate::i18n::t;
|
||||||
use anyhow::{Context, Ok, Result};
|
use anyhow::{Context, Ok, Result};
|
||||||
|
|
||||||
pub fn parse_response(json_text: &str) -> Result<HeartbeatResponse> {
|
pub fn parse_response(json_text: &str) -> Result<HeartbeatResponse> {
|
||||||
let mut response: HeartbeatResponse =
|
let mut response: HeartbeatResponse = serde_json::from_slice(json_text.as_bytes())
|
||||||
serde_json::from_str(json_text).with_context(|| t("invalid-json-heartbeat"))?;
|
.with_context(|| t("invalid-json-heartbeat"))?;
|
||||||
|
|
||||||
response.process()?;
|
response.process()?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use crate::i18n::t;
|
||||||
use anyhow::{Context, Ok, Result};
|
use anyhow::{Context, Ok, Result};
|
||||||
|
|
||||||
pub fn parse_response(json_text: &str) -> Result<StatusPageResponse> {
|
pub fn parse_response(json_text: &str) -> Result<StatusPageResponse> {
|
||||||
let response: StatusPageResponse =
|
let response: StatusPageResponse = serde_json::from_slice(json_text.as_bytes())
|
||||||
serde_json::from_str(json_text).with_context(|| t("invalid-json-status-page"))?;
|
.with_context(|| t("invalid-json-status-page"))?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ pub fn t(key: &str) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = LOCALES.lookup(&*get_system_locale(), key);
|
let result = LOCALES.lookup(get_system_locale(), key);
|
||||||
|
|
||||||
let mut cache_write = cache.write().unwrap();
|
let mut cache_write = cache.write().unwrap();
|
||||||
|
|
||||||
|
|
@ -48,12 +48,8 @@ pub fn t(key: &str) -> String {
|
||||||
|
|
||||||
pub fn t_with_args(key: &str, args: &HashMap<&'static str, String>) -> String {
|
pub fn t_with_args(key: &str, args: &HashMap<&'static str, String>) -> String {
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
let args_map: &HashMap<Cow<'static, _>, FluentValue<'_>>;
|
|
||||||
args_map = {
|
|
||||||
for (key, value) in args {
|
for (key, value) in args {
|
||||||
map.insert(Cow::Borrowed(*key), FluentValue::from(value.clone()));
|
map.insert(Cow::Borrowed(*key), FluentValue::from(value));
|
||||||
}
|
}
|
||||||
&map
|
LOCALES.lookup_with_args(get_system_locale(), key, &map)
|
||||||
};
|
|
||||||
LOCALES.lookup_with_args(&get_system_locale(), key, args_map)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ Response = Response
|
||||||
response = response
|
response = response
|
||||||
invalid-json-status-page = ❌ Error parssing status page JSON
|
invalid-json-status-page = ❌ Error parssing status page JSON
|
||||||
invalid-json-heartbeat = ❌ Error parssing heartbeat JSON
|
invalid-json-heartbeat = ❌ Error parssing heartbeat JSON
|
||||||
invalid-uptime-key-format = Invalid format for uptime key. Expected format "monitorID_period". Received key: {key}
|
invalid-uptime-key-format = Invalid format for uptime key. Expected format "monitorID_period". Received key: {$key}
|
||||||
invalid-monitor-id = Invalid monitor ID: {id}
|
invalid-monitor-id = Invalid monitor ID: {$id}
|
||||||
invalid-period-hours = Invalid period in hours: {hours}
|
invalid-period-hours = Invalid period in hours: {$hours}
|
||||||
loading = Loading...
|
loading = Loading...
|
||||||
monitors = Monitors
|
monitors = Monitors
|
||||||
unknown = Unknown
|
unknown = Unknown
|
||||||
|
|
@ -18,11 +18,11 @@ status = Status
|
||||||
error = Error
|
error = Error
|
||||||
dashboard-header = Status Dashboard
|
dashboard-header = Status Dashboard
|
||||||
never = Never
|
never = Never
|
||||||
auto-update-failed = Automatic update failed: {error}
|
auto-update-failed = Automatic update failed: {$error}
|
||||||
update-fail = Failed to update data: {error}
|
update-fail = Failed to update data: {$error}
|
||||||
now = Now
|
now = Now
|
||||||
uptime = Uptime
|
uptime = Uptime
|
||||||
history = History
|
history = History
|
||||||
auto-update-enabled = Auto-update enabled ({interval} min)
|
auto-update-enabled = Auto-update enabled ({$interval} min)
|
||||||
update-failed = Update failed: {error}
|
update-failed = Update failed: {$error}
|
||||||
key-to-exit = Press 'q' to exit
|
key-to-exit = Press 'q' to exit
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ missing_url = ❌ URL não fornecida. Use --url ou a variável de ambiente UPTIM
|
||||||
Response = Resposta
|
Response = Resposta
|
||||||
response = resposta
|
response = resposta
|
||||||
invalid-json-status-page = ❌ Falha ao parsear JSON do status page
|
invalid-json-status-page = ❌ Falha ao parsear JSON do status page
|
||||||
invalid-json-heartbeat = ❌ Falha ao parsear JSON do heartbeat
|
invalid-json-heartbeat = Falha ao parsear JSON do heartbeat
|
||||||
invalid-uptime-key-format = Formato inválido na chave de uptime. Chave esperada no formato "monitorID_periodo". Chave recebida: {key}
|
invalid-uptime-key-format = Formato inválido na chave de uptime. Chave esperada no formato "monitorID_periodo". Chave recebida: {$key}
|
||||||
invalid-monitor-id = ID de monitor inválido: {id}
|
invalid-monitor-id = ID de monitor inválido: {$id}
|
||||||
invalid-period-hours = Período em horas inválido: {hours}
|
invalid-period-hours = Período em horas inválido: {$hours}
|
||||||
loading = Carregando...
|
loading = Carregando...
|
||||||
monitors = Monitors
|
monitors = Monitors
|
||||||
monitor = Monitor
|
monitor = Monitor
|
||||||
|
|
@ -23,6 +23,6 @@ auto-update-failed = Falha na atualização automática
|
||||||
update-fail = Falha ao atualizar dados
|
update-fail = Falha ao atualizar dados
|
||||||
now = Agora
|
now = Agora
|
||||||
history = Historico
|
history = Historico
|
||||||
auto-update-enabled = Auto-atualização ativada ({interval} min)
|
auto-update-enabled = Auto-atualização ativada ({$interval} min)
|
||||||
update-failed = Falha na atualização: {error}
|
update-failed = Falha na atualização: {$error}
|
||||||
key-to-exit = Pressione 'q' para sair
|
key-to-exit = Pressione 'q' para sair
|
||||||
|
|
|
||||||
164
src/ui/app.rs
164
src/ui/app.rs
|
|
@ -1,12 +1,10 @@
|
||||||
use crate::api::{UptimeKumaClient, UptimeKumaEndpoints};
|
use crate::api::{UptimeKumaClient, UptimeKumaEndpoints};
|
||||||
use crate::core;
|
use crate::core;
|
||||||
use crate::data::{heartbeat::HeartbeatResponse, status_page::model::StatusPageResponse};
|
|
||||||
use crate::i18n::{t, t_with_args};
|
use crate::i18n::{t, t_with_args};
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
components::{render_footer, render_header, render_monitor_list},
|
components::{render_footer, render_header, render_monitor_list},
|
||||||
dashboard::model::DashboardViewState,
|
dashboard::model::DashboardViewState,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||||
execute,
|
execute,
|
||||||
|
|
@ -18,23 +16,18 @@ use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::{Line, Span, Text},
|
text::{Line, Span, Text},
|
||||||
widgets::{Block, Borders, Padding, Paragraph},
|
widgets::{Block, Borders, Padding, Paragraph, Scrollbar, ScrollbarOrientation},
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
io,
|
io,
|
||||||
sync::mpsc,
|
|
||||||
thread,
|
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
const INITIAL_INTERVAL: u32 = 300;
|
const INITIAL_INTERVAL: u32 = 300;
|
||||||
|
const MAIN_LAYOUT_WITH_SCROLLBAR: [Constraint; 2] = [Constraint::Min(1), Constraint::Length(1)];
|
||||||
enum FetchResult {
|
const MAIN_LAYOUT_WITHOUT_SCROLLBAR: [Constraint; 1] = [Constraint::Min(1)];
|
||||||
Heartbeat(Result<HeartbeatResponse>),
|
|
||||||
StatusPage(Result<StatusPageResponse>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
state: DashboardViewState,
|
state: DashboardViewState,
|
||||||
|
|
@ -47,6 +40,26 @@ pub struct App {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
pub fn new(endpoints: UptimeKumaEndpoints) -> io::Result<Self> {
|
||||||
|
let backend = CrosstermBackend::new(io::stdout());
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
terminal.hide_cursor()?;
|
||||||
|
|
||||||
|
let state = DashboardViewState::new();
|
||||||
|
|
||||||
|
let initial_interval = Duration::from_secs(INITIAL_INTERVAL as u64);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
state,
|
||||||
|
terminal,
|
||||||
|
should_quit: false,
|
||||||
|
last_update: Instant::now(),
|
||||||
|
update_interval: initial_interval,
|
||||||
|
endpoints,
|
||||||
|
client: UptimeKumaClient::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn setup_terminal(&mut self) -> io::Result<()> {
|
fn setup_terminal(&mut self) -> io::Result<()> {
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?;
|
execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
|
|
@ -116,53 +129,103 @@ impl App {
|
||||||
fn render(&mut self) {
|
fn render(&mut self) {
|
||||||
let _ = self.terminal.draw(|frame| {
|
let _ = self.terminal.draw(|frame| {
|
||||||
let area = frame.area();
|
let area = frame.area();
|
||||||
|
|
||||||
|
const HEADER_HEIGHT: u16 = 3;
|
||||||
|
const FOOTER_HEIGHT: u16 = 3;
|
||||||
|
|
||||||
|
let max_content_height = area.height.saturating_sub(HEADER_HEIGHT + FOOTER_HEIGHT);
|
||||||
|
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.margin(1)
|
.margin(1)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Length(3),
|
Constraint::Length(HEADER_HEIGHT),
|
||||||
Constraint::Min(1),
|
Constraint::Length(max_content_height.max(1)),
|
||||||
Constraint::Length(3),
|
Constraint::Length(FOOTER_HEIGHT),
|
||||||
])
|
])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
render_header(frame, chunks[0], &self.state);
|
render_header(frame, chunks[0], &self.state);
|
||||||
|
|
||||||
if self.state.is_loading {
|
let main_constraint = if self.state.show_vertical_scrollbar(chunks[1].height) {
|
||||||
Self::render_loading(frame, chunks[1]);
|
&MAIN_LAYOUT_WITH_SCROLLBAR[..]
|
||||||
} else if let Some(error) = &self.state.error_message {
|
|
||||||
Self::render_error(frame, chunks[1], error);
|
|
||||||
} else if self.state.groups.is_empty() || self.state.get_all_monitors().is_empty() {
|
|
||||||
Self::render_no_data(frame, chunks[1]);
|
|
||||||
} else {
|
} else {
|
||||||
render_monitor_list(frame, chunks[1], &self.state);
|
&MAIN_LAYOUT_WITHOUT_SCROLLBAR[..]
|
||||||
|
};
|
||||||
|
|
||||||
|
let main_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(main_constraint)
|
||||||
|
.split(chunks[1]);
|
||||||
|
|
||||||
|
if self.state.is_loading {
|
||||||
|
Self::render_loading(frame, main_chunks[0]);
|
||||||
|
} else if let Some(error) = &self.state.error_message {
|
||||||
|
Self::render_error(frame, main_chunks[0], error);
|
||||||
|
} else if self.state.groups.is_empty() || self.state.get_all_monitors().is_empty() {
|
||||||
|
Self::render_no_data(frame, main_chunks[0]);
|
||||||
|
} else {
|
||||||
|
Self::render_main(frame, main_chunks.to_vec(), &mut self.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
let seconds_until_update = self
|
let seconds_until_update = self
|
||||||
.update_interval
|
.update_interval
|
||||||
.checked_sub(self.last_update.elapsed())
|
.checked_sub(self.last_update.elapsed())
|
||||||
.map(|d| d.as_secs() as u64)
|
.map(|d| d.as_secs())
|
||||||
.unwrap_or(30);
|
.unwrap_or(30);
|
||||||
|
|
||||||
render_footer(frame, chunks[2], seconds_until_update);
|
render_footer(frame, chunks[2], seconds_until_update);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_main(frame: &mut Frame, area: Vec<Rect>, state: &mut DashboardViewState) {
|
||||||
|
render_monitor_list(frame, area[0], state);
|
||||||
|
|
||||||
|
if !state.show_vertical_scrollbar(area[0].height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if area.len() <= 1 {
|
||||||
|
dbg!(area[0].height);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_scroll = state
|
||||||
|
.get_total_length()
|
||||||
|
.saturating_sub(area[0].height as usize)
|
||||||
|
.saturating_add(1);
|
||||||
|
|
||||||
|
state.scroll_state = state
|
||||||
|
.scroll_state
|
||||||
|
.content_length(max_scroll.max(1))
|
||||||
|
.viewport_content_length(area[0].height as usize);
|
||||||
|
|
||||||
|
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||||
|
.begin_symbol(Some("↑"))
|
||||||
|
.end_symbol(Some("↓"));
|
||||||
|
|
||||||
|
frame.render_stateful_widget(scrollbar, area[1], &mut state.scroll_state);
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_events(&mut self) -> io::Result<()> {
|
fn handle_events(&mut self) -> io::Result<()> {
|
||||||
let timeout = Duration::from_secs(1);
|
let timeout = Duration::from_secs(1);
|
||||||
|
|
||||||
if event::poll(timeout)? {
|
if event::poll(timeout)?
|
||||||
if let Event::Key(key) = event::read()? {
|
&& let Event::Key(key) = event::read()?
|
||||||
|
{
|
||||||
if key.kind == KeyEventKind::Release {
|
if key.kind == KeyEventKind::Release {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
|
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
|
||||||
|
KeyCode::Up | KeyCode::Char('k') => self.state.scroll_state.prev(),
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => self.state.scroll_state.next(),
|
||||||
|
KeyCode::Home => self.state.scroll_state.first(),
|
||||||
|
KeyCode::End => self.state.scroll_state.last(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,7 +243,6 @@ impl App {
|
||||||
|
|
||||||
fn refresh_data(&mut self) -> io::Result<()> {
|
fn refresh_data(&mut self) -> io::Result<()> {
|
||||||
self.state.is_loading = true;
|
self.state.is_loading = true;
|
||||||
self.render();
|
|
||||||
|
|
||||||
match self.fetch_and_update_data() {
|
match self.fetch_and_update_data() {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
|
@ -197,57 +259,11 @@ impl App {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(endpoints: UptimeKumaEndpoints) -> io::Result<Self> {
|
|
||||||
let backend = CrosstermBackend::new(io::stdout());
|
|
||||||
let mut terminal = Terminal::new(backend)?;
|
|
||||||
terminal.hide_cursor()?;
|
|
||||||
|
|
||||||
let state = DashboardViewState::new();
|
|
||||||
|
|
||||||
let initial_interval = Duration::from_secs(INITIAL_INTERVAL as u64);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
state,
|
|
||||||
terminal,
|
|
||||||
should_quit: false,
|
|
||||||
last_update: Instant::now(),
|
|
||||||
update_interval: initial_interval,
|
|
||||||
endpoints,
|
|
||||||
client: UptimeKumaClient::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch_and_update_data(&mut self) -> anyhow::Result<()> {
|
fn fetch_and_update_data(&mut self) -> anyhow::Result<()> {
|
||||||
let (tx, rx) = mpsc::channel();
|
let heartbeat_result = self.client.fetch_heartbeat(&self.endpoints)?;
|
||||||
|
let status_page_result = self.client.fetch_status_page(&self.endpoints)?;
|
||||||
|
|
||||||
let heartbeat_client = self.client.clone();
|
let unified_data = core::unify_data(&status_page_result, &heartbeat_result);
|
||||||
let heartbeat_endpoints = self.endpoints.clone();
|
|
||||||
let tx_clone = tx.clone();
|
|
||||||
thread::spawn(move || {
|
|
||||||
let result = heartbeat_client.fetch_heartbeat(&heartbeat_endpoints);
|
|
||||||
tx.send(FetchResult::Heartbeat(result)).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
let status_page_client = self.client.clone();
|
|
||||||
let status_page_endpoints = self.endpoints.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
let result = status_page_client.fetch_status_page(&status_page_endpoints);
|
|
||||||
tx_clone.send(FetchResult::StatusPage(result)).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut heartbeat_result = None;
|
|
||||||
let mut status_page_result = None;
|
|
||||||
|
|
||||||
for _ in 0..2 {
|
|
||||||
match rx.recv()? {
|
|
||||||
FetchResult::Heartbeat(result) => heartbeat_result = Some(result?),
|
|
||||||
FetchResult::StatusPage(result) => status_page_result = Some(result?),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let heartbeat_data = heartbeat_result.unwrap();
|
|
||||||
let status_page_data = status_page_result.unwrap();
|
|
||||||
let unified_data = core::unify_data(&status_page_data, &heartbeat_data);
|
|
||||||
self.state = DashboardViewState::from_unified_data(unified_data);
|
self.state = DashboardViewState::from_unified_data(unified_data);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ use ratatui::{
|
||||||
use crate::i18n::t;
|
use crate::i18n::t;
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
|
|
||||||
pub fn render_footer(frame: &mut Frame, area: Rect, seconds_until_update: u64) {
|
pub fn render_footer(
|
||||||
|
frame: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
seconds_until_update: u64
|
||||||
|
) {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
let datatime_str = now.format("%Y-%m-%d %H:%M:%S").to_string();
|
let datatime_str = now.format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
let countdown_str = format!("↻ {}s", seconds_until_update);
|
let countdown_str = format!("↻ {}s", seconds_until_update);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
use std::cmp::min;
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
use crate::i18n::t;
|
cmp::min,
|
||||||
use crate::ui::dashboard::{
|
collections::{HashMap, hash_map::DefaultHasher},
|
||||||
MonitorStatus, MonitorViewState,
|
hash::{Hash, Hasher},
|
||||||
model::{DashboardViewState, GroupViewState},
|
sync::{OnceLock, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
i18n::t,
|
||||||
|
ui::dashboard::{
|
||||||
|
MonitorStatus, MonitorViewState,
|
||||||
|
model::{BORDER_LINES_VIEW, DashboardViewState, GroupViewState},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
|
|
@ -16,50 +25,72 @@ use ratatui::{
|
||||||
const STATUS_LINE_LENGTH: usize = 100;
|
const STATUS_LINE_LENGTH: usize = 100;
|
||||||
const MAX_NAME_LENGTH: usize = 30;
|
const MAX_NAME_LENGTH: usize = 30;
|
||||||
|
|
||||||
pub fn render_monitor_list(frame: &mut Frame, area: Rect, state: &DashboardViewState) {
|
static UP_SPAN: Lazy<Span<'static>> =
|
||||||
let group_areas = layout_groups(area, &state.groups);
|
Lazy::new(|| Span::styled("■", Style::default().fg(Color::Green)));
|
||||||
|
|
||||||
for (i, (group, &group_area)) in state.groups.iter().zip(group_areas.iter()).enumerate() {
|
static DOWN_SPAN: Lazy<Span<'static>> =
|
||||||
render_group(frame, group_area, group, i == 0);
|
Lazy::new(|| Span::styled("■", Style::default().fg(Color::Red)));
|
||||||
}
|
|
||||||
|
static UNKNOWN_SPAN: Lazy<Span<'static>> =
|
||||||
|
Lazy::new(|| Span::styled("■", Style::default().fg(Color::Yellow)));
|
||||||
|
|
||||||
|
static STATUS_LINE_CACHE: OnceLock<RwLock<HashMap<u64, Line<'static>>>> = OnceLock::new();
|
||||||
|
|
||||||
|
pub fn render_monitor_list(main_frame: &mut Frame, area: Rect, state: &mut DashboardViewState) {
|
||||||
|
let available_height = area.height as usize;
|
||||||
|
let max_scroll = state.get_total_length().saturating_sub(available_height);
|
||||||
|
|
||||||
|
if state.scroll_state.get_position() > max_scroll {
|
||||||
|
state.scroll_state = state.scroll_state.position(max_scroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout_groups(area: Rect, groups: &[GroupViewState]) -> Vec<Rect> {
|
let scroll_pos = state.scroll_state.get_position();
|
||||||
let total_lines: usize = groups.iter().map(|g| g.monitors.len() + 1).sum();
|
let mut current_y = area.y as usize;
|
||||||
|
let mut rendered_height = 0;
|
||||||
|
let mut lines_skipped = 0;
|
||||||
|
let half = state.get_total_length().saturating_div(2);
|
||||||
|
|
||||||
if total_lines == 0 {
|
for group in state.groups.iter() {
|
||||||
return Vec::new();
|
let group_height = group.monitors.len() + BORDER_LINES_VIEW;
|
||||||
|
|
||||||
|
if lines_skipped + group_height <= scroll_pos {
|
||||||
|
lines_skipped += group_height;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content_height = area.height.saturating_sub(2);
|
let visible_height = if lines_skipped < scroll_pos {
|
||||||
let line_height = content_height as usize / total_lines;
|
group_height - (scroll_pos - lines_skipped)
|
||||||
|
} else {
|
||||||
|
group_height
|
||||||
|
}
|
||||||
|
.min(available_height - rendered_height);
|
||||||
|
|
||||||
let mut current_y = area.y + 1;
|
let group_area = Rect {
|
||||||
let mut areas = Vec::with_capacity(groups.len());
|
|
||||||
|
|
||||||
for group in groups {
|
|
||||||
let group_lines = group.monitors.len() + 2;
|
|
||||||
let group_height = (group_lines + line_height).max(1);
|
|
||||||
areas.push(Rect {
|
|
||||||
x: area.x,
|
x: area.x,
|
||||||
y: current_y,
|
y: current_y as u16,
|
||||||
width: area.width,
|
width: area.width,
|
||||||
height: group_height as u16,
|
height: visible_height as u16,
|
||||||
});
|
};
|
||||||
|
|
||||||
current_y += group_height as u16;
|
render_group(main_frame, group_area, group, half > rendered_height);
|
||||||
|
|
||||||
|
current_y += visible_height;
|
||||||
|
rendered_height += visible_height;
|
||||||
|
lines_skipped += group_height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
areas
|
fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first_half: bool) {
|
||||||
}
|
|
||||||
|
|
||||||
fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first: bool) {
|
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.margin(0)
|
.margin(0)
|
||||||
.constraints([Constraint::Length(1), Constraint::Min(1)])
|
.constraints([Constraint::Length(1), Constraint::Min(1)])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
|
if chunks[0].height == 0 || chunks[1].height == 0 || group.monitors.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let group_title = Line::from(vec![
|
let group_title = Line::from(vec![
|
||||||
Span::styled(format!("{} ", group.name), title_style()),
|
Span::styled(format!("{} ", group.name), title_style()),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
|
|
@ -69,11 +100,7 @@ fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first:
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let title_block = Block::default()
|
let title_block = Block::default()
|
||||||
.borders(if is_first {
|
.borders(Borders::ALL)
|
||||||
Borders::TOP | Borders::LEFT | Borders::RIGHT
|
|
||||||
} else {
|
|
||||||
Borders::ALL
|
|
||||||
})
|
|
||||||
.border_style(Style::default().fg(Color::Blue))
|
.border_style(Style::default().fg(Color::Blue))
|
||||||
.title(group_title)
|
.title(group_title)
|
||||||
.title_alignment(Alignment::Left);
|
.title_alignment(Alignment::Left);
|
||||||
|
|
@ -81,13 +108,24 @@ fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first:
|
||||||
frame.render_widget(title_block, chunks[0]);
|
frame.render_widget(title_block, chunks[0]);
|
||||||
|
|
||||||
if !group.monitors.is_empty() {
|
if !group.monitors.is_empty() {
|
||||||
render_monitor_table(frame, chunks[1], &group.monitors);
|
render_monitor_table(frame, chunks[1], &group.monitors, is_first_half);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_monitor_table(frame: &mut Frame, area: Rect, monitors: &Vec<MonitorViewState>) {
|
fn render_monitor_table(
|
||||||
|
frame: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
monitors: &[MonitorViewState],
|
||||||
|
is_first_half: bool,
|
||||||
|
) {
|
||||||
let max_items = area.height as usize;
|
let max_items = area.height as usize;
|
||||||
let items_to_show = min(monitors.len(), max_items);
|
let items_to_show = min(monitors.len(), max_items);
|
||||||
|
let index = if is_first_half {
|
||||||
|
monitors.len().saturating_sub(max_items.saturating_sub(2)) // 2 = Table header + botton
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let monitors = &monitors[index..index + items_to_show.min(monitors.len() - index)];
|
||||||
|
|
||||||
let header_cells = vec![
|
let header_cells = vec![
|
||||||
"".to_string(),
|
"".to_string(),
|
||||||
|
|
@ -99,11 +137,10 @@ fn render_monitor_table(frame: &mut Frame, area: Rect, monitors: &Vec<MonitorVie
|
||||||
|
|
||||||
let header = Row::new(header_cells).style(title_style()).height(1);
|
let header = Row::new(header_cells).style(title_style()).height(1);
|
||||||
|
|
||||||
let rows: Vec<Row> = monitors
|
let mut rows: Vec<Row> = Vec::with_capacity(items_to_show);
|
||||||
.iter()
|
for monitor in monitors.iter().take(items_to_show) {
|
||||||
.take(items_to_show)
|
rows.push(create_monitor_item(monitor));
|
||||||
.map(|monitor| create_monitor_item(monitor))
|
}
|
||||||
.collect();
|
|
||||||
|
|
||||||
let widths = vec![
|
let widths = vec![
|
||||||
Constraint::Length(3),
|
Constraint::Length(3),
|
||||||
|
|
@ -127,14 +164,6 @@ fn render_monitor_table(frame: &mut Frame, area: Rect, monitors: &Vec<MonitorVie
|
||||||
frame.render_widget(table, area);
|
frame.render_widget(table, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_status_char(status: &MonitorStatus) -> char {
|
|
||||||
match status {
|
|
||||||
MonitorStatus::Up => '■',
|
|
||||||
MonitorStatus::Down => '■',
|
|
||||||
MonitorStatus::Unknown => '■',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_status_color(status: &MonitorStatus) -> Color {
|
pub fn get_status_color(status: &MonitorStatus) -> Color {
|
||||||
match status {
|
match status {
|
||||||
MonitorStatus::Up => Color::Green,
|
MonitorStatus::Up => Color::Green,
|
||||||
|
|
@ -143,11 +172,14 @@ pub fn get_status_color(status: &MonitorStatus) -> Color {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_status_emoji(status: &MonitorStatus) -> &str {
|
pub fn get_status_emoji(status: &MonitorStatus) -> Cow<'static, str> {
|
||||||
|
static UP_ICON: &str = "✅";
|
||||||
|
static DOWN_ICON: &str = "❌";
|
||||||
|
static UNKNOWN_ICON: &str = "❓";
|
||||||
match status {
|
match status {
|
||||||
MonitorStatus::Up => "✅",
|
MonitorStatus::Up => Cow::Borrowed(UP_ICON),
|
||||||
MonitorStatus::Down => "❌",
|
MonitorStatus::Down => Cow::Borrowed(DOWN_ICON),
|
||||||
MonitorStatus::Unknown => "❓",
|
MonitorStatus::Unknown => Cow::Borrowed(UNKNOWN_ICON),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,20 +187,24 @@ fn create_monitor_item(monitor: &MonitorViewState) -> Row<'_> {
|
||||||
let status_icon = get_status_emoji(&monitor.status);
|
let status_icon = get_status_emoji(&monitor.status);
|
||||||
let status_color = get_status_color(&monitor.status);
|
let status_color = get_status_color(&monitor.status);
|
||||||
|
|
||||||
let display_name: String = if monitor.name.len() > MAX_NAME_LENGTH {
|
let display_name: Cow<str> = if monitor.name.len() > MAX_NAME_LENGTH {
|
||||||
format!("{:.width$}...", &monitor.name, width = MAX_NAME_LENGTH - 3)
|
Cow::Owned(format!(
|
||||||
|
"{:.width$}...",
|
||||||
|
monitor.name,
|
||||||
|
width = MAX_NAME_LENGTH - 3
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
monitor.name.clone()
|
Cow::Borrowed(&monitor.name)
|
||||||
};
|
};
|
||||||
|
|
||||||
let response_text = format!("{:>7}ms", monitor.response_time);
|
let response_text = format!("{:>7}ms", monitor.response_time);
|
||||||
let uptime_text = format!("{:>7}%", monitor.uptime_24h);
|
let uptime_text = format!("{:>7}%", monitor.uptime_24h);
|
||||||
|
|
||||||
let status_line_spans = create_status_line_spans(&monitor.status_history);
|
let status_line_spans = get_cached_status_line(&monitor.status_history);
|
||||||
|
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
get_formated_line(format!("{} ", status_icon), status_color, Modifier::empty()),
|
get_formated_line(format!("{} ", status_icon), status_color, Modifier::empty()),
|
||||||
get_formated_line(display_name, Color::White, Modifier::empty()),
|
get_formated_line(display_name.to_string(), Color::White, Modifier::empty()),
|
||||||
get_formated_line(response_text, Color::Cyan, Modifier::empty()),
|
get_formated_line(response_text, Color::Cyan, Modifier::empty()),
|
||||||
get_formated_line(uptime_text, Color::Magenta, Modifier::empty()),
|
get_formated_line(uptime_text, Color::Magenta, Modifier::empty()),
|
||||||
status_line_spans,
|
status_line_spans,
|
||||||
|
|
@ -184,22 +220,12 @@ fn get_formated_line(text: String, color: Color, modifier: Modifier) -> Line<'st
|
||||||
)])
|
)])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_status_line_spans(status_history: &[MonitorStatus]) -> Line<'_> {
|
fn get_status_span(status: &MonitorStatus) -> &'static Span<'static> {
|
||||||
let recent_status: Vec<_> = status_history
|
match status {
|
||||||
.iter()
|
MonitorStatus::Up => &UP_SPAN,
|
||||||
.rev()
|
MonitorStatus::Down => &DOWN_SPAN,
|
||||||
.take(STATUS_LINE_LENGTH)
|
MonitorStatus::Unknown => &UNKNOWN_SPAN,
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut spans = Vec::with_capacity(recent_status.len());
|
|
||||||
|
|
||||||
for status in recent_status.iter().rev() {
|
|
||||||
let c = get_status_char(status);
|
|
||||||
let color = get_status_color(status);
|
|
||||||
|
|
||||||
spans.push(Span::styled(c.to_string(), Style::default().fg(color)));
|
|
||||||
}
|
}
|
||||||
Line::from(spans)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn title_style() -> Style {
|
fn title_style() -> Style {
|
||||||
|
|
@ -207,3 +233,46 @@ fn title_style() -> Style {
|
||||||
.fg(Color::Yellow)
|
.fg(Color::Yellow)
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn calculate_history_hash(status_history: &[MonitorStatus]) -> u64 {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
status_history
|
||||||
|
.iter()
|
||||||
|
.take(STATUS_LINE_LENGTH)
|
||||||
|
.for_each(|status| {
|
||||||
|
status.hash(&mut hasher);
|
||||||
|
});
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cached_status_line(status_history: &[MonitorStatus]) -> Line<'static> {
|
||||||
|
let hash = calculate_history_hash(status_history);
|
||||||
|
let cache = STATUS_LINE_CACHE.get_or_init(|| RwLock::new(HashMap::new()));
|
||||||
|
{
|
||||||
|
let read = cache.read().unwrap();
|
||||||
|
if let Some(line) = read.get(&hash) {
|
||||||
|
return line.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut spans: Vec<Span<'static>> = Vec::with_capacity(STATUS_LINE_LENGTH);
|
||||||
|
spans.extend(
|
||||||
|
status_history
|
||||||
|
.iter()
|
||||||
|
.take(STATUS_LINE_LENGTH)
|
||||||
|
.map(|status| get_status_span(status).clone()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_line = Line::from(spans);
|
||||||
|
let mut write = cache.write().unwrap();
|
||||||
|
if write.len() > 1000 {
|
||||||
|
let mut keys_to_remove: Vec<u64> = Vec::with_capacity(250);
|
||||||
|
keys_to_remove.extend(write.keys().take(250).copied());
|
||||||
|
|
||||||
|
for key in keys_to_remove {
|
||||||
|
write.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write.insert(hash, new_line.clone());
|
||||||
|
new_line
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
use crate::core::models::{UnifiedData, UnifiedGroupData};
|
use crate::core::models::{UnifiedData, UnifiedGroupData};
|
||||||
use crate::data::heartbeat::model::HeartbeatEntry;
|
use crate::data::heartbeat::model::HeartbeatEntry;
|
||||||
use crate::i18n::t;
|
use crate::i18n::t;
|
||||||
|
use ratatui::widgets::ScrollbarState;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
pub const BORDER_LINES_VIEW: usize = 3;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum MonitorStatus {
|
pub enum MonitorStatus {
|
||||||
Up,
|
Up,
|
||||||
Down,
|
Down,
|
||||||
|
|
@ -12,7 +15,7 @@ pub enum MonitorStatus {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MonitorViewState {
|
pub struct MonitorViewState {
|
||||||
pub name: String,
|
pub name: Cow<'static, str>,
|
||||||
pub status: MonitorStatus,
|
pub status: MonitorStatus,
|
||||||
pub response_time: String,
|
pub response_time: String,
|
||||||
pub uptime_24h: String,
|
pub uptime_24h: String,
|
||||||
|
|
@ -33,6 +36,8 @@ pub struct DashboardViewState {
|
||||||
pub is_loading: bool,
|
pub is_loading: bool,
|
||||||
pub error_message: Option<String>,
|
pub error_message: Option<String>,
|
||||||
pub auto_refresh_interval: u32,
|
pub auto_refresh_interval: u32,
|
||||||
|
pub scroll_state: ScrollbarState,
|
||||||
|
total_length: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DashboardViewState {
|
impl DashboardViewState {
|
||||||
|
|
@ -44,6 +49,8 @@ impl DashboardViewState {
|
||||||
is_loading: true,
|
is_loading: true,
|
||||||
error_message: None,
|
error_message: None,
|
||||||
auto_refresh_interval: 300,
|
auto_refresh_interval: 300,
|
||||||
|
scroll_state: ScrollbarState::new(0),
|
||||||
|
total_length: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,32 +64,43 @@ impl DashboardViewState {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let total_length: usize = groups
|
||||||
|
.iter()
|
||||||
|
.map(|g| g.monitors.len() + BORDER_LINES_VIEW)
|
||||||
|
.sum();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
title: data.title,
|
title: data.title.into_owned(),
|
||||||
descriptions: data.description,
|
descriptions: data.description.map(|d| d.into_owned()),
|
||||||
groups,
|
groups,
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
error_message: None,
|
error_message: None,
|
||||||
auto_refresh_interval: data.auto_refresh_interval.max(30),
|
auto_refresh_interval: data.auto_refresh_interval.max(30),
|
||||||
|
scroll_state: ScrollbarState::new(total_length.saturating_sub(1)),
|
||||||
|
total_length,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_total_length(&self) -> usize {
|
||||||
|
self.total_length
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_all_monitors(&self) -> Vec<&MonitorViewState> {
|
pub fn get_all_monitors(&self) -> Vec<&MonitorViewState> {
|
||||||
self.groups.iter().flat_map(|g| g.monitors.iter()).collect()
|
self.groups.iter().flat_map(|g| g.monitors.iter()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn show_vertical_scrollbar(&self, available_height: u16) -> bool {
|
||||||
|
self.total_length as u16 > available_height
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec<MonitorStatus> {
|
fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec<MonitorStatus> {
|
||||||
let mut history = heartbeats
|
let mut history: Vec<_> = Vec::with_capacity(heartbeats.len());
|
||||||
.iter()
|
history.extend(heartbeats.iter().rev().take(100).map(|h| match h.status {
|
||||||
.rev()
|
|
||||||
.take(100)
|
|
||||||
.map(|h| match h.status {
|
|
||||||
0 => MonitorStatus::Down,
|
0 => MonitorStatus::Down,
|
||||||
1 => MonitorStatus::Up,
|
1 => MonitorStatus::Up,
|
||||||
_ => MonitorStatus::Unknown,
|
_ => MonitorStatus::Unknown,
|
||||||
})
|
}));
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
while history.len() < 100 {
|
while history.len() < 100 {
|
||||||
history.push(MonitorStatus::Unknown);
|
history.push(MonitorStatus::Unknown);
|
||||||
|
|
@ -94,9 +112,11 @@ fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec<MonitorStatus> {
|
||||||
|
|
||||||
fn add_monitor_view_state(group: UnifiedGroupData) -> Vec<MonitorViewState> {
|
fn add_monitor_view_state(group: UnifiedGroupData) -> Vec<MonitorViewState> {
|
||||||
let mut monitors = Vec::with_capacity(group.monitors.len());
|
let mut monitors = Vec::with_capacity(group.monitors.len());
|
||||||
group.monitors.into_par_iter().map(|monitor| {
|
group
|
||||||
|
.monitors
|
||||||
let status_history = get_status_history(&monitor.heartbeats);
|
.into_par_iter()
|
||||||
|
.map(|monitor| {
|
||||||
|
let status_history = get_status_history(monitor.heartbeats);
|
||||||
|
|
||||||
let status = match monitor.heartbeats.last().map(|h| h.status) {
|
let status = match monitor.heartbeats.last().map(|h| h.status) {
|
||||||
Some(1) => MonitorStatus::Up,
|
Some(1) => MonitorStatus::Up,
|
||||||
|
|
@ -104,26 +124,30 @@ fn add_monitor_view_state(group: UnifiedGroupData) -> Vec<MonitorViewState> {
|
||||||
_ => MonitorStatus::Unknown,
|
_ => MonitorStatus::Unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response_time = monitor
|
let response_time = match monitor.heartbeats.last().and_then(|h| h.ping) {
|
||||||
.heartbeats
|
Some(ms) => format!("{}", ms),
|
||||||
.last()
|
None => t("unknown"),
|
||||||
.and_then(|h| h.ping)
|
};
|
||||||
.map(|ms| format!("{}", ms))
|
|
||||||
.unwrap_or_else(|| t("unknown").to_string() + " ");
|
|
||||||
|
|
||||||
let uptime_24h = monitor
|
let uptime_24h = match monitor.uptime_data.map(|u| u.get_perc_formated()) {
|
||||||
.uptime_data
|
Some(perc) => perc,
|
||||||
.map(|u| u.get_perc_formated())
|
None => t("unknown"),
|
||||||
.unwrap_or_else(|| t("unknown").to_string());
|
};
|
||||||
|
|
||||||
|
let name: Cow<'static, str> = match monitor.name {
|
||||||
|
Cow::Borrowed(borrowed) => Cow::Owned(borrowed.to_string()),
|
||||||
|
Cow::Owned(owned) => Cow::Owned(owned),
|
||||||
|
};
|
||||||
|
|
||||||
MonitorViewState {
|
MonitorViewState {
|
||||||
name: monitor.name,
|
name,
|
||||||
status,
|
status,
|
||||||
response_time,
|
response_time,
|
||||||
uptime_24h,
|
uptime_24h,
|
||||||
status_history,
|
status_history,
|
||||||
}
|
}
|
||||||
}).collect_into_vec(&mut monitors);
|
})
|
||||||
|
.collect_into_vec(&mut monitors);
|
||||||
|
|
||||||
monitors.sort_by_key(|m| m.name.clone());
|
monitors.sort_by_key(|m| m.name.clone());
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue