diff --git a/.forgejo/workflows/release.yaml b/.forgejo/workflows/release.yaml deleted file mode 100644 index e62f186..0000000 --- a/.forgejo/workflows/release.yaml +++ /dev/null @@ -1,179 +0,0 @@ -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: "Release ${{ forgejo.ref_name }}" - 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- - # 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) diff --git a/Cargo.lock b/Cargo.lock index 62df022..8002565 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "atomic" @@ -249,9 +249,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.57" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -259,9 +259,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.57" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -271,9 +271,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -2668,7 +2668,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "uptime-kuma-dashboard" -version = "0.38.2" +version = "0.1.0" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index acc8090..1610b56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,16 @@ [package] name = "uptime-kuma-dashboard" -version = "0.38.2" +version = "0.1.0" edition = "2024" [dependencies] -clap = { version = "4.5.57", features = ["derive", "env"] } +clap = { version = "4.5.54", features = ["derive", "env"] } reqwest = { version = "0.13.1", default-features = false, features = [ "blocking", "json", "rustls", ] } -anyhow = "1.0.101" +anyhow = "1.0" fluent-templates = "0.13.2" unic-langid = "0.9.6" sys-locale = "0.3.2" diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 732499f..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -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. diff --git a/README.md b/README.md deleted file mode 100644 index bbfca7e..0000000 --- a/README.md +++ /dev/null @@ -1,145 +0,0 @@ -# Uptime Kuma Terminal Dashboard - -![Terminal Dashboard Preview](https://img.shields.io/badge/Rust-1.76+-orange?style=flat-square&logo=rust) - -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 - - - -## 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!_ diff --git a/assets/demo.webm b/assets/demo.webm deleted file mode 100644 index fb10b24..0000000 Binary files a/assets/demo.webm and /dev/null differ diff --git a/src/api/client.rs b/src/api/client.rs index 6dc4448..eb3032f 100644 --- a/src/api/client.rs +++ b/src/api/client.rs @@ -1,11 +1,9 @@ -use std::time::Duration; - use anyhow::Result; use reqwest::blocking::Client; use crate::{ api::endpoints::UptimeKumaEndpoints, - data::{self, heartbeat::HeartbeatResponse, status_page::model::StatusPageResponse}, i18n::t, + data::{self, heartbeat::HeartbeatResponse, status_page::model::StatusPageResponse}, }; #[derive(Debug, Clone)] @@ -15,9 +13,8 @@ pub struct UptimeKumaClient { impl UptimeKumaClient { 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 { - client, + client: Client::new(), } } diff --git a/src/ui/app.rs b/src/ui/app.rs index 2e19131..a2abab4 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,10 +1,12 @@ use crate::api::{UptimeKumaClient, UptimeKumaEndpoints}; use crate::core; +use crate::data::{heartbeat::HeartbeatResponse, status_page::model::StatusPageResponse}; use crate::i18n::{t, t_with_args}; use crate::ui::{ components::{render_footer, render_header, render_monitor_list}, dashboard::model::DashboardViewState, }; +use anyhow::Result; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, execute, @@ -22,6 +24,8 @@ use ratatui::{ use std::{ collections::HashMap, io, + sync::mpsc, + thread, time::{Duration, Instant}, }; @@ -29,6 +33,11 @@ const INITIAL_INTERVAL: u32 = 300; const MAIN_LAYOUT_WITH_SCROLLBAR: [Constraint; 2] = [Constraint::Min(1), Constraint::Length(1)]; const MAIN_LAYOUT_WITHOUT_SCROLLBAR: [Constraint; 1] = [Constraint::Min(1)]; +enum FetchResult { + Heartbeat(Result), + StatusPage(Result), +} + pub struct App { state: DashboardViewState, terminal: Terminal>, @@ -40,26 +49,6 @@ pub struct App { } impl App { - pub fn new(endpoints: UptimeKumaEndpoints) -> io::Result { - 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<()> { enable_raw_mode()?; execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?; @@ -191,7 +180,7 @@ impl App { } let max_scroll = state - .get_total_length() + .get_total_lenght() .saturating_sub(area[0].height as usize) .saturating_add(1); @@ -259,11 +248,57 @@ impl App { Ok(()) } - fn fetch_and_update_data(&mut self) -> anyhow::Result<()> { - let heartbeat_result = self.client.fetch_heartbeat(&self.endpoints)?; - let status_page_result = self.client.fetch_status_page(&self.endpoints)?; + pub fn new(endpoints: UptimeKumaEndpoints) -> io::Result { + let backend = CrosstermBackend::new(io::stdout()); + let mut terminal = Terminal::new(backend)?; + terminal.hide_cursor()?; - let unified_data = core::unify_data(&status_page_result, &heartbeat_result); + 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<()> { + let (tx, rx) = mpsc::channel(); + + let heartbeat_client = self.client.clone(); + 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); Ok(()) } diff --git a/src/ui/components/monitor_list.rs b/src/ui/components/monitor_list.rs index ae89733..1cab664 100644 --- a/src/ui/components/monitor_list.rs +++ b/src/ui/components/monitor_list.rs @@ -38,7 +38,7 @@ static STATUS_LINE_CACHE: OnceLock>>> = OnceLo 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); + let max_scroll = state.get_total_lenght().saturating_sub(available_height); if state.scroll_state.get_position() > max_scroll { state.scroll_state = state.scroll_state.position(max_scroll); @@ -48,7 +48,7 @@ pub fn render_monitor_list(main_frame: &mut Frame, area: Rect, state: &mut Dashb 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); + let half = state.get_total_lenght().saturating_div(2); for group in state.groups.iter() { let group_height = group.monitors.len() + BORDER_LINES_VIEW; @@ -259,6 +259,7 @@ fn get_cached_status_line(status_history: &[MonitorStatus]) -> Line<'static> { spans.extend( status_history .iter() + .rev() .take(STATUS_LINE_LENGTH) .map(|status| get_status_span(status).clone()), ); diff --git a/src/ui/dashboard/model.rs b/src/ui/dashboard/model.rs index 6425d2b..a59595a 100644 --- a/src/ui/dashboard/model.rs +++ b/src/ui/dashboard/model.rs @@ -81,7 +81,7 @@ impl DashboardViewState { } } - pub fn get_total_length(&self) -> usize { + pub fn get_total_lenght(&self) -> usize { self.total_length }