Compare commits

..

72 commits
v0.0.2 ... main

Author SHA1 Message Date
150d83be42 Fix typo 2026-02-10 11:55:01 -04:00
0ad49775fc Adding demo 2026-02-10 11:53:04 -04:00
cc54502a12 chore: Release uptime-kuma-dashboard version 0.38.2
All checks were successful
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 7m57s
Multi-Platform Release Build / build (true, build-essential gcc-x86-64-linux-gnu libc6-dev-amd64-cross pkg-config binutils-x86-64-linux-gnu, map[CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:x86_64-linux-gnu-gcc], linux/amd64, x86_64-unknown-linux-gnu) (push) Successful in 16m31s
Multi-Platform Release Build / build (true, gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 18m1s
Multi-Platform Release Build / create-release (push) Successful in 45s
2026-02-07 14:34:40 -04:00
80bf20cda1 Fix typo 2026-02-07 14:34:04 -04:00
88138341ae chore: Release uptime-kuma-dashboard version 0.38.1
All checks were successful
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 15m1s
Multi-Platform Release Build / build (true, gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 22m50s
Multi-Platform Release Build / build (true, build-essential gcc-x86-64-linux-gnu libc6-dev-amd64-cross pkg-config binutils-x86-64-linux-gnu, map[CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:x86_64-linux-gnu-gcc], linux/amd64, x86_64-unknown-linux-gnu) (push) Successful in 19m9s
Multi-Platform Release Build / create-release (push) Successful in 1m9s
2026-02-05 15:43:41 -04:00
436eac2600 Fix heartbeats order 2026-02-05 15:43:15 -04:00
c39fed294b chore: Release uptime-kuma-dashboard version 0.38.0
All checks were successful
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 13m7s
Multi-Platform Release Build / build (true, build-essential gcc-x86-64-linux-gnu libc6-dev-amd64-cross pkg-config binutils-x86-64-linux-gnu, map[CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:x86_64-linux-gnu-gcc], linux/amd64, x86_64-unknown-linux-gnu) (push) Successful in 19m5s
Multi-Platform Release Build / build (true, gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 24m33s
Multi-Platform Release Build / create-release (push) Successful in 48s
2026-01-22 16:35:09 -04:00
441687f67d Adding LICENSE 2026-01-22 16:25:48 -04:00
e7e4509016 Update README.md 2026-01-22 12:32:10 -04:00
c5f272b70e Added README and production release workflow 2026-01-22 09:46:38 -04:00
17662107b0 Refactoring 2026-01-21 12:34:50 -04:00
d8b4843415 optimizations 2026-01-20 16:54:36 -04:00
99d4b94e5c optimizations 2026-01-20 16:18:54 -04:00
64f5115d54 Line optimizations 2026-01-19 16:04:45 -04:00
975c4b1eaf Span optimizations 2026-01-19 13:34:37 -04:00
d63c701867 Update dependencies 2026-01-19 10:31:18 -04:00
c48a37d56a Clippy suggestions 2026-01-19 06:28:25 -04:00
00cb8f7a9b Refactor 2026-01-18 15:43:40 -04:00
70a70af859 Refactor cache 2026-01-15 08:23:20 -04:00
512f597a0f Fix 2026-01-12 16:38:06 -04:00
389f2b886b Small optimizations 2026-01-12 14:37:52 -04:00
3d69f530d4 String Optizations 2026-01-12 12:37:28 -04:00
3929a4a277 String optimizations 2026-01-12 11:49:37 -04:00
6109785e63 Small fixes
All checks were successful
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 13m20s
Multi-Platform Release Build / build (true, build-essential gcc-x86-64-linux-gnu libc6-dev-amd64-cross pkg-config binutils-x86-64-linux-gnu, map[CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:x86_64-linux-gnu-gcc], linux/amd64, x86_64-unknown-linux-gnu) (push) Successful in 10m58s
Multi-Platform Release Build / build (true, gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 13m31s
Multi-Platform Release Build / create-release (push) Successful in 1m1s
2026-01-12 04:03:12 -04:00
60e4fa3b9d Fixing scrollbar view content 2026-01-11 14:06:12 -04:00
000e31a14f Working scrollbar 2026-01-10 11:19:19 -04:00
df1d1dddc6 Scrollbar initial values 2026-01-09 11:56:35 -04:00
4e17ef1e09 Small fix 2026-01-08 14:30:22 -04:00
74464f6a48 Partial fix where height is lower than total monitors, but there is no
area for scrollbar (area.len() == 1)
2026-01-08 13:33:37 -04:00
142f76684b Show vertical scrollbar only if height is lower than total monitors 2026-01-08 09:25:26 -04:00
d4e3ce6e27 Update Cargo.toml 2026-01-07 10:33:16 -04:00
fba0d17403 Testing workflow
All checks were successful
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 6m15s
Multi-Platform Release Build / build (true, build-essential gcc-x86-64-linux-gnu libc6-dev-amd64-cross pkg-config binutils-x86-64-linux-gnu, map[CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:x86_64-linux-gnu-gcc], linux/amd64, x86_64-unknown-linux-gnu) (push) Successful in 8m2s
Multi-Platform Release Build / build (true, gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 11m3s
Multi-Platform Release Build / create-release (push) Successful in 41s
2026-01-05 16:40:35 -04:00
a8467a3cf1 Testing workflow
All checks were successful
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 6m7s
Multi-Platform Release Build / build (true, build-essential gcc-x86-64-linux-gnu libc6-dev-amd64-cross pkg-config binutils-x86-64-linux-gnu, map[CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:x86_64-linux-gnu-gcc], linux/amd64, x86_64-unknown-linux-gnu) (push) Successful in 9m15s
Multi-Platform Release Build / build (true, gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 10m35s
Multi-Platform Release Build / create-release (push) Successful in 41s
2026-01-05 15:53:04 -04:00
78d0e553af Testing workflow
All checks were successful
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 5m43s
Multi-Platform Release Build / build (true, build-essential gcc-x86-64-linux-gnu libc6-dev-amd64-cross pkg-config binutils-x86-64-linux-gnu, map[CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:x86_64-linux-gnu-gcc], linux/amd64, x86_64-unknown-linux-gnu) (push) Successful in 8m34s
Multi-Platform Release Build / build (true, gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 10m50s
Multi-Platform Release Build / create-release (push) Successful in 40s
2026-01-05 14:15:51 -04:00
89bf217afd Update type 2026-01-05 13:52:50 -04:00
349e0cf3ff Refactoring 2026-01-05 13:51:55 -04:00
4eb4c64398 Testing workflow
All checks were successful
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 5m28s
Multi-Platform Release Build / build (true, build-essential gcc-x86-64-linux-gnu libc6-dev-amd64-cross pkg-config binutils-x86-64-linux-gnu, map[CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:x86_64-linux-gnu-gcc], linux/amd64, x86_64-unknown-linux-gnu) (push) Successful in 8m31s
Multi-Platform Release Build / build (true, gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 10m14s
Multi-Platform Release Build / create-release (push) Successful in 39s
2026-01-05 11:50:50 -04:00
12e3a30325 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 5m31s
Multi-Platform Release Build / build (true, build-essential gcc-x86-64-linux-gnu libc6-dev-amd64-cross pkg-config binutils-x86-64-linux-gnu, map[CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:x86_64-linux-gnu-gcc], linux/amd64, x86_64-unknown-linux-gnu) (push) Successful in 16m40s
Multi-Platform Release Build / build (true, gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 11m21s
Multi-Platform Release Build / create-release (push) Failing after 57s
2026-01-05 11:05:06 -04:00
97b16624b6 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 6m18s
Multi-Platform Release Build / build (true, build-essential gcc-x86-64-linux-gnu libc6-dev-amd64-cross pkg-config binutils-x86-64-linux-gnu, map[CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:x86_64-linux-gnu-gcc], linux/amd64, x86_64-unknown-linux-gnu) (push) Successful in 19m54s
Multi-Platform Release Build / build (true, gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 17m50s
Multi-Platform Release Build / create-release (push) Failing after 2m2s
2026-01-04 16:41:18 -04:00
4347996e2b Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 13m4s
Multi-Platform Release Build / build (true, build-essential gcc-x86-64-linux-gnu libc6-dev-amd64-cross pkg-config, map[CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:x86_64-linux-gnu-gcc], linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 14m6s
Multi-Platform Release Build / build (true, gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 19m24s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-04 15:31:47 -04:00
d80bc544dc Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 5m34s
Multi-Platform Release Build / build (true, gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake nasm, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Failing after 7m35s
Multi-Platform Release Build / build (true, gcc-x86-64-linux-gnu libc6-dev-amd64-cross, map[CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER:x86_64-linux-gnu-gcc], linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 7m23s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-04 11:35:44 -04:00
60a1a87d04 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 8m58s
Multi-Platform Release Build / build (true, build-essential gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Failing after 8m48s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 15m55s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-04 07:57:15 -04:00
2a6b233909 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 9m42s
Multi-Platform Release Build / build (true, build-essential gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Failing after 11m23s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 14m58s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-03 14:15:35 -04:00
d3ff4d3916 Update Cargo.toml 2026-01-03 13:50:01 -04:00
5a77198344 Fix translation error 2026-01-03 08:45:13 -04:00
6e94081db2 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 5m22s
Multi-Platform Release Build / build (true, build-essential gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 13m0s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 8m11s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-02 14:31:05 -04:00
3c95626cf9 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 5m17s
Multi-Platform Release Build / build (true, build-essential gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 12m30s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 2m33s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-02 11:45:30 -04:00
7bb7a5a6b1 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 6m2s
Multi-Platform Release Build / build (true, build-essential gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 12m26s
Multi-Platform Release Build / build (true, build-essential libssl-dev pkg-config gcc-x86_64-linux-gnu, map[OPENSSL_INCLUDE_DIR:/usr/include/openssl OPENSSL_LIB_DIR:/usr/lib/x86_64-linux-gnu OPENSSL_STATIC:1 PKG_CONFIG_ALLOW_CROSS:1 PKG_CONFIG_ALL_STATIC:1], linux/amd64, x86_64-unkn… (push) Failing after 5m50s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-02 10:12:51 -04:00
a31c7a4a3e Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 5m32s
Multi-Platform Release Build / build (true, build-essential gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 12m31s
Multi-Platform Release Build / build (true, build-essential libssl-dev pkg-config, map[OPENSSL_DIR:/usr PKG_CONFIG_ALLOW_CROSS:1 PKG_CONFIG_PATH:/usr/lib/pkgconfig:/usr/share/pkgconfig], linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 5m59s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-02 09:04:30 -04:00
845c49f664 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 5m35s
Multi-Platform Release Build / build (true, build-essential gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Successful in 17m27s
Multi-Platform Release Build / build (true, build-essential libssl-dev pkg-config, map[OPENSSL_DIR:/usr PKG_CONFIG_ALLOW_CROSS:1 PKG_CONFIG_PATH:/usr/lib/pkgconfig:/usr/share/pkgconfig], linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 4m29s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-02 07:16:37 -04:00
e5938b9014 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 10m13s
Multi-Platform Release Build / build (true, build-essential gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64, map[CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER:x86_64-w64-mingw32-gcc CC_x86_64_pc_windows_gnu:x86_64-w64-mingw32-gcc], linux/amd64, x86_64-pc-windows-gnu) (push) Failing after 3m58s
Multi-Platform Release Build / build (true, build-essential libssl-dev pkg-config, map[OPENSSL_DIR:/usr PKG_CONFIG_ALLOW_CROSS:1 PKG_CONFIG_PATH:/usr/lib/pkgconfig:/usr/share/pkgconfig], linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 9m48s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-02 06:47:40 -04:00
ca1442851c Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 5m22s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-pc-windows-gnu) (push) Failing after 4m1s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 5m30s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-01 19:17:53 -04:00
415032ecd1 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 4m58s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-pc-windows-gnu) (push) Failing after 2m36s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 5m41s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-01 17:47:56 -04:00
99f143fcc2 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 10m34s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-pc-windows-gnu) (push) Failing after 4m6s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 3m49s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-01 15:25:40 -04:00
a59a7ab7ce Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 5m28s
Multi-Platform Release Build / build (true, build-essential libssl-dev pkg-config, linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 2m30s
Multi-Platform Release Build / build (true, build-essential mingw-w64, linux/amd64, x86_64-pc-windows-gnu) (push) Failing after 2m5s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-01 15:03:58 -04:00
1de1cdd9b9 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Successful in 5m44s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-pc-windows-gnu) (push) Failing after 5m45s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 8m3s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-01 08:36:34 -04:00
1de395dfc0 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Failing after 11s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-pc-windows-gnu) (push) Failing after 1s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 1s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-01 08:34:48 -04:00
041e5622b6 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, linux/arm64, native) (push) Failing after 14s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-pc-windows-gnu) (push) Failing after 0s
Multi-Platform Release Build / build (true, linux/amd64, x86_64-unknown-linux-gnu) (push) Failing after 1s
Multi-Platform Release Build / create-release (push) Has been skipped
2026-01-01 07:44:31 -04:00
6bd9d5080c Testing workflow
Some checks failed
Multi-Platform Release Build / build (, false, ARM64 NATIVE, native) (push) Successful in 5m20s
Multi-Platform Release Build / build (ghcr.io/forgejo-actions/rust-cross:x86_64-pc-windows-gnu-2025, true, Windows x86_64, x86_64-pc-windows-gnu) (push) Failing after 1s
Multi-Platform Release Build / build (ghcr.io/forgejo-actions/rust-cross:x86_64-unknown-linux-gnu-2025, true, Linux x86_64, x86_64-unknown-linux-gnu) (push) Failing after 0s
Multi-Platform Release Build / create-release (push) Has been skipped
2025-12-31 14:28:42 -04:00
390ec63af8 Testing workflow 2025-12-31 14:25:52 -04:00
8857104e64 Testing workflow 2025-12-31 14:21:32 -04:00
f3f88fcdbf Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, aarch64-linux-gnu-gcc, aarch64-unknown-linux-gnu) (push) Successful in 11m27s
Multi-Platform Release Build / build (false, native) (push) Successful in 4m56s
Multi-Platform Release Build / build (true, x86_64-linux-gnu-gcc, x86_64-unknown-linux-gnu) (push) Failing after 2m6s
Multi-Platform Release Build / build (true, x86_64-w64-mingw32-gcc, x86_64-pc-windows-gnu) (push) Failing after 2m0s
Multi-Platform Release Build / create-release (push) Has been skipped
2025-12-31 13:11:24 -04:00
239f9529d5 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, native) (push) Successful in 10m47s
Multi-Platform Release Build / build (true, aarch64-linux-gnu-gcc, aarch64-unknown-linux-gnu) (push) Failing after 1m27s
Multi-Platform Release Build / build (true, arm-linux-gnueabihf-gcc, armv7-unknown-linux-gnueabihf) (push) Failing after 1m38s
Multi-Platform Release Build / build (true, x86_64-linux-gnu-gcc, x86_64-unknown-linux-gnu) (push) Failing after 1m54s
Multi-Platform Release Build / build (true, x86_64-w64-mingw32-gcc, x86_64-pc-windows-gnu) (push) Failing after 1m27s
Multi-Platform Release Build / create-release (push) Has been skipped
2025-12-31 12:24:21 -04:00
f99e7dafae Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, native) (push) Failing after 8m40s
Multi-Platform Release Build / build (true, aarch64-linux-gnu-gcc, aarch64-unknown-linux-gnu) (push) Failing after 1m47s
Multi-Platform Release Build / build (true, arm-linux-gnueabihf-gcc, armv7-unknown-linux-gnueabihf) (push) Failing after 1m56s
Multi-Platform Release Build / build (true, x86_64-linux-gnu-gcc, x86_64-unknown-linux-gnu) (push) Failing after 1m51s
Multi-Platform Release Build / build (true, x86_64-w64-mingw32-gcc, x86_64-pc-windows-gnu) (push) Failing after 1m28s
Multi-Platform Release Build / create-release (push) Has been skipped
2025-12-31 09:07:38 -04:00
06af1d284f Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, native) (push) Failing after 8m50s
Multi-Platform Release Build / build (true, aarch64-linux-gnu-gcc, aarch64-unknown-linux-gnu) (push) Failing after 2m0s
Multi-Platform Release Build / build (true, arm-linux-gnueabihf-gcc, armv7-unknown-linux-gnueabihf) (push) Failing after 1m42s
Multi-Platform Release Build / build (true, x86_64-linux-gnu-gcc, x86_64-unknown-linux-gnu) (push) Failing after 2m7s
Multi-Platform Release Build / build (true, x86_64-w64-mingw32-gcc, x86_64-pc-windows-gnu) (push) Failing after 2m9s
Multi-Platform Release Build / create-release (push) Has been skipped
2025-12-31 08:21:46 -04:00
98da3e0c05 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, native) (push) Failing after 12m51s
Multi-Platform Release Build / build (true, aarch64-linux-gnu-gcc, aarch64-unknown-linux-gnu) (push) Failing after 1m54s
Multi-Platform Release Build / build (true, arm-linux-gnueabihf-gcc, armv7-unknown-linux-gnueabihf) (push) Failing after 1m37s
Multi-Platform Release Build / build (true, x86_64-linux-gnu-gcc, x86_64-unknown-linux-gnu) (push) Failing after 1m38s
Multi-Platform Release Build / build (true, x86_64-w64-mingw32-gcc, x86_64-pc-windows-gnu) (push) Failing after 2m1s
Multi-Platform Release Build / create-release (push) Has been skipped
2025-12-31 07:28:55 -04:00
7082ce4dfc Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, native) (push) Failing after 3s
Multi-Platform Release Build / build (true, aarch64-linux-gnu-gcc, aarch64-unknown-linux-gnu) (push) Failing after 3s
Multi-Platform Release Build / build (true, arm-linux-gnueabihf-gcc, armv7-unknown-linux-gnueabihf) (push) Failing after 3s
Multi-Platform Release Build / build (true, x86_64-linux-gnu-gcc, x86_64-unknown-linux-gnu) (push) Failing after 2s
Multi-Platform Release Build / build (true, x86_64-w64-mingw32-gcc, x86_64-pc-windows-gnu) (push) Failing after 11s
Multi-Platform Release Build / create-release (push) Has been skipped
2025-12-30 15:28:46 -04:00
e3fe669cc5 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, native) (push) Failing after 3m44s
Multi-Platform Release Build / build (true, aarch64-linux-gnu-gcc, aarch64-unknown-linux-gnu) (push) Failing after 21s
Multi-Platform Release Build / build (true, arm-linux-gnueabihf-gcc, armv7-unknown-linux-gnueabihf) (push) Failing after 7s
Multi-Platform Release Build / build (true, x86_64-linux-gnu-gcc, x86_64-unknown-linux-gnu) (push) Failing after 7s
Multi-Platform Release Build / build (true, x86_64-w64-mingw32-gcc, x86_64-pc-windows-gnu) (push) Failing after 7s
Multi-Platform Release Build / create-release (push) Has been skipped
2025-12-30 14:48:21 -04:00
e1b7b570c5 Testing workflow
Some checks failed
Multi-Platform Release Build / build (false, native) (push) Failing after 3m34s
Multi-Platform Release Build / build (true, aarch64-linux-gnu-gcc, aarch64-unknown-linux-gnu) (push) Failing after 30s
Multi-Platform Release Build / build (true, arm-linux-gnueabihf-gcc, armv7-unknown-linux-gnueabihf) (push) Failing after 11s
Multi-Platform Release Build / build (true, x86_64-linux-gnu-gcc, x86_64-unknown-linux-gnu) (push) Failing after 11s
Multi-Platform Release Build / build (true, x86_64-w64-mingw32-gcc, x86_64-pc-windows-gnu) (push) Failing after 14s
Multi-Platform Release Build / create-release (push) Has been skipped
2025-12-30 13:51:55 -04:00
5e8732c513 Testing workflow
Some checks failed
Multi-Platform Release Build / detect-architecture (push) Successful in 3m31s
Multi-Platform Release Build / build (false, native) (push) Failing after 28s
Multi-Platform Release Build / build (true, aarch64-linux-gnu-gcc, aarch64-unknown-linux-gnu) (push) Failing after 4s
Multi-Platform Release Build / build (true, arm-linux-gnueabihf-gcc, armv7-unknown-linux-gnueabihf) (push) Failing after 4s
Multi-Platform Release Build / build (true, x86_64-linux-gnu-gcc, x86_64-unknown-linux-gnu) (push) Failing after 4s
Multi-Platform Release Build / build (true, x86_64-w64-mingw32-gcc, x86_64-pc-windows-gnu) (push) Failing after 3s
Multi-Platform Release Build / create-release (push) Has been skipped
2025-12-30 12:32:53 -04:00
4735d649d2 Testing workflow
Some checks failed
Multi-Platform Release Build / detect-architecture (push) Has been cancelled
Multi-Platform Release Build / build (false, native) (push) Has been cancelled
Multi-Platform Release Build / build (true, aarch64-linux-gnu-gcc, aarch64-unknown-linux-gnu) (push) Has been cancelled
Multi-Platform Release Build / build (true, arm-linux-gnueabihf-gcc, armv7-unknown-linux-gnueabihf) (push) Has been cancelled
Multi-Platform Release Build / build (true, x86_64-linux-gnu-gcc, x86_64-unknown-linux-gnu) (push) Has been cancelled
Multi-Platform Release Build / build (true, x86_64-w64-mingw32-gcc, x86_64-pc-windows-gnu) (push) Has been cancelled
Multi-Platform Release Build / create-release (push) Has been cancelled
2025-12-30 11:38:09 -04:00
6e989deb09 Testing workflow 2025-12-30 11:35:49 -04:00
22 changed files with 1299 additions and 680 deletions

View 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)

View file

@ -1,74 +1,45 @@
name: Multi-Platform Release Build
on:
push:
tags:
- "v*.*.*"
jobs:
detect-architecture:
runs-on: ubuntu-latest
outputs:
runner_arch: ${{ env.RUNNER_ARCH }}
steps:
- name: Simply output architecture
run: echo "Runner architecture: $RUNNER_ARCH"
name: Debug env variables
run: env | grep -i arch
build:
runs-on: ubuntu-latest
needs: detect-architecture
runs-on: docker
strategy:
fail-fast: false
matrix:
include:
- target: native
cross: false
platform: linux/arm64
- target: x86_64-unknown-linux-gnu
cross: true
linker: x86_64-linux-gnu-gcc
- target: aarch64-unknown-linux-gnu
cross: true
linker: aarch64-linux-gnu-gcc
- target: armv7-unknown-linux-gnueabihf
cross: true
linker: arm-linux-gnueabihf-gcc
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
linker: x86_64-w64-mingw32-gcc
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: actions/checkout@v4
uses: https://code.forgejo.org/actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Add Rust target
if: matrix.target != 'native'
run: rustup target add ${{ matrix.target }}
- name: Install cross-compilation tools
if: matrix.cross
run: |
sudo apt-get update
sudo apt-get install -y \
gcc-arm-linux-gnueabihf \
gcc-aarch64-linux-gnu \
gcc-x86-64-linux-gnu \
g++-arm-linux-gnueabihf \
g++-aarch64-linux-gnu \
mingw-w64 \
libc6-dev-armhf-cross \
libc6-dev-arm64-cross
- name: Cache dependencies
uses: actions/cache@v3
- name: Cache Rust dependencies
uses: https://code.forgejo.org/actions/cache@v4
with:
path: |
~/.cargo/bin/
@ -81,82 +52,75 @@ jobs:
${{ runner.os }}-cargo-${{ matrix.target }}-
${{ runner.os }}-cargo-
- name: Configure cross-compilation
if: matrix.cross && matrix.target != 'native'
- name: Native Build Release
if: matrix.target == 'native'
run: |
mkdir -p .cargo
cat >> .cargo/config.toml << EOF
[target.${{ matrix.target }}]
linker = "${{ matrix.linker }}"
EOF
- name: Build release
run: |
if [ "${{ matrix.target }}" = "native" ]; then
# Build nativo para a arquitetura do runner
NATIVE_TARGET=$(rustc -vV | grep 'host:' | cut -d' ' -f2)
echo "Building for native target: $NATIVE_TARGET"
cargo build --release --features production
TARGET_BINARY="target/release/uptime-kuma-dashboard"
OUTPUT_NAME="uptime-kuma-dashboard-$NATIVE_TARGET"
else
# Build com target específico
echo "Building for target: ${{ matrix.target }}"
cargo build --release --target ${{ matrix.target }} --features production
if [[ "${{ matrix.target }}" == *"windows"* ]]; then
TARGET_BINARY="target/${{ matrix.target }}/release/uptime-kuma-dashboard.exe"
OUTPUT_NAME="uptime-kuma-dashboard-${{ matrix.target }}.exe"
else
TARGET_BINARY="target/${{ matrix.target }}/release/uptime-kuma-dashboard"
OUTPUT_NAME="uptime-kuma-dashboard-${{ matrix.target }}"
fi
fi
# Criar diretório de artifacts
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
# Verificar se o binário foi criado
if [ ! -f "$TARGET_BINARY" ]; then
echo "Error: Binary not found at $TARGET_BINARY"
exit 1
fi
# Copiar binário
cp "$TARGET_BINARY" "release-artifacts/$OUTPUT_NAME"
# Strip binário para Linux (reduz tamanho)
if [[ "${{ matrix.target }}" == *"linux"* ]] || [ "${{ matrix.target }}" = "native" ]; then
strip "release-artifacts/$OUTPUT_NAME" 2>/dev/null || echo "Strip failed, continuing..."
fi
# Tornar executável
if [[ "${{ matrix.target }}" != *"windows"* ]]; then
chmod +x "release-artifacts/$OUTPUT_NAME"
fi
# Mostrar informações do arquivo
strip "release-artifacts/$OUTPUT_NAME" 2>/dev/null || true
chmod +x "release-artifacts/$OUTPUT_NAME"
ls -lh "release-artifacts/$OUTPUT_NAME"
file "release-artifacts/$OUTPUT_NAME" || true
- 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: actions/upload-artifact@v4
uses: https://data.forgejo.org/forgejo/upload-artifact@v4
with:
name: binary-${{ matrix.target }}
path: release-artifacts/
retention-days: 1
create-release:
runs-on: ubuntu-latest
needs: [detect-architecture, build]
runs-on: docker
needs: build
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: https://code.forgejo.org/actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
uses: https://data.forgejo.org/forgejo/download-artifact@v4
with:
path: all-artifacts/
@ -173,53 +137,43 @@ jobs:
cat SHA256SUMS.txt
- name: Create Release
uses: actions/gitea-release-action@v1
uses: https://code.forgejo.org/actions/forgejo-release@v2
with:
files: release-artifacts/*
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ github.ref_name }}
name: Release ${{ github.ref_name }}
body: |
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 runner **${{ needs.detect-architecture.outputs.runner_arch }}**
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, Apple Silicon Linux, ARM servers
- **Linux ARMv7** (armhf) - Raspberry Pi 3 and earlier
- **Linux ARM64** (aarch64) - Raspberry Pi 4, ARM servers
- **Windows x86_64** (64-bit)
### 🚀 How to use
#### Linux/ARM:
```bash
# 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
```
# 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:
```powershell
# 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
```
# 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
```bash
sha256sum -c SHA256SUMS.txt
```
sha256sum -c SHA256SUMS.txt
### 🏗️ Build Information
- Runner Architecture: ${{ needs.detect-architecture.outputs.runner_arch }}
- Rust Version: stable
- Build Type: Release (optimized)
- Rust Version: ${{ steps.rust-version.outputs.version }}
- Build Date: $(date +'%Y-%m-%d')
- Build Type: Release (optimized with LTO)

34
.vscode/launch.json vendored Normal file
View 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

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,28 @@
[package]
name = "uptime-kuma-dashboard"
version = "0.1.0"
version = "0.38.2"
edition = "2024"
[dependencies]
clap = { version = "4.5", features = ["derive", "env"] }
reqwest = { version = "0.12.28", features = ["blocking", "json"] }
anyhow = "1.0"
clap = { version = "4.5.57", features = ["derive", "env"] }
reqwest = { version = "0.13.1", default-features = false, features = [
"blocking",
"json",
"rustls",
] }
anyhow = "1.0.101"
fluent-templates = "0.13.2"
unic-langid = "0.9.6"
sys-locale = "0.3.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.148"
serde_json = "1.0.149"
once_cell = "1.19"
url = "2.5.7"
url = "2.5.8"
ratatui = "0.30.0"
crossterm = "0.29.0"
chrono = "0.4.42"
chrono = "0.4.43"
rayon = "1.11.0"
rustls = { version = "0.23.36", default-features = false, features = ["ring"] }
[features]
default = []
@ -33,7 +38,7 @@ lto = true
codegen-units = 1
strip = true
panic = "abort"
debug = false
debug = false
[profile.release-min]
inherits = "release"

21
LICENSE Normal file
View 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
View file

@ -0,0 +1,145 @@
# 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
<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

Binary file not shown.

View file

@ -1,9 +1,11 @@
use std::time::Duration;
use anyhow::Result;
use reqwest::blocking::Client;
use crate::{
api::endpoints::UptimeKumaEndpoints,
data::{self, heartbeat::HeartbeatResponse, status_page::model::StatusPageResponse},
data::{self, heartbeat::HeartbeatResponse, status_page::model::StatusPageResponse}, i18n::t,
};
#[derive(Debug, Clone)]
@ -13,29 +15,37 @@ 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::new(),
client,
}
}
pub fn fetch_heartbeat(&self, endpoints: &UptimeKumaEndpoints) -> Result<HeartbeatResponse> {
let response = self.client.get(endpoints.heartbeat_url()).send()?;
if response.status().is_success() {
let json_text = response.text()?;
data::heartbeat::parse_response(&json_text)
} else {
return Err(anyhow::anyhow!(response.status()));
}
self.fetch_and_parse(endpoints.heartbeat_url(), data::heartbeat::parse_response)
}
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() {
let json_text = response.text()?;
data::status_page::parse_response(&json_text)
parser(&json_text)
} else {
return Err(anyhow::anyhow!(response.status()));
Err(anyhow::anyhow!(
"URL: {}, Error: {}",
url,
response.status()
))
}
}
}

View file

@ -16,10 +16,28 @@ impl UptimeKumaEndpoints {
}
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 {
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
}
}

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::collections::HashMap;
use crate::core::models::{UnifiedData, UnifiedGroupData, UnifiedMonitorData};
@ -6,32 +7,45 @@ use crate::data::{
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 heartbeat_map: HashMap<u64, Vec<HeartbeatEntry>> = heartbeat
.monitors
.iter()
.map(|m| (m.monitor_id, m.heartbeats.clone()))
.collect();
let uptime_map: HashMap<(u64, u32), UptimeData> = heartbeat
.uptime_data
.iter()
.map(|u| ((u.monitor_id, u.period_hours), u.clone()))
.collect();
let mut heartbeat_map: HashMap<u64, &'a [HeartbeatEntry]> =
HashMap::with_capacity(heartbeat.monitors.len());
heartbeat_map.extend(
heartbeat
.monitors
.iter()
.map(|m| (m.monitor_id, &m.heartbeats[..])),
);
let mut uptime_map: HashMap<(u64, u32), &'a UptimeData> =
HashMap::with_capacity(heartbeat.uptime_data.len());
uptime_map.extend(
heartbeat
.uptime_data
.iter()
.map(|u| ((u.monitor_id, u.period_hours), u)),
);
for group in &status_page.public_group_list {
let mut monitors = Vec::with_capacity(group.monitor_list.len());
for monitor_info in &group.monitor_list {
let uptime_data = uptime_map.get(&(monitor_info.id, 24)).cloned();
let heartbeats = heartbeat_map
.get(&monitor_info.id)
.cloned()
.unwrap_or_default();
let uptime_data = uptime_map.get(&(monitor_info.id, 24)).copied();
let heartbeats = heartbeat_map.get(&monitor_info.id).copied().unwrap_or(&[]);
let name: Cow<'a, str> = if monitor_info.name.len() > 100 {
Cow::Owned(monitor_info.name.clone())
} else {
Cow::Borrowed(monitor_info.name.as_str())
};
monitors.push(UnifiedMonitorData {
id: monitor_info.id,
name: monitor_info.name.clone(),
name,
heartbeats,
uptime_data,
});
@ -40,7 +54,7 @@ pub fn unify_data(status_page: &StatusPageResponse, heartbeat: &HeartbeatRespons
monitors.sort_by_key(|m| m.id);
groups.push(UnifiedGroupData {
group_info: group.clone(),
group_info: group,
monitors,
});
}
@ -48,8 +62,8 @@ pub fn unify_data(status_page: &StatusPageResponse, heartbeat: &HeartbeatRespons
groups.sort_by_key(|g| g.group_info.weight);
UnifiedData {
title: status_page.config.title.clone(),
description: status_page.config.description.clone(),
title: Cow::Borrowed(&status_page.config.title),
description: status_page.config.description.as_deref().map(Cow::Borrowed),
groups,
auto_refresh_interval: status_page.config.auto_refresh_interval,
}

View file

@ -1,24 +1,26 @@
use std::borrow::Cow;
use crate::data::heartbeat::model::{HeartbeatEntry, UptimeData};
use crate::data::status_page::model::{StatusPageGroup};
#[derive(Debug, Clone)]
pub struct UnifiedMonitorData {
pub struct UnifiedMonitorData<'a> {
pub id: u64,
pub name: String,
pub heartbeats: Vec<HeartbeatEntry>,
pub uptime_data: Option<UptimeData>,
pub name: Cow<'a, str>,
pub heartbeats: &'a [HeartbeatEntry],
pub uptime_data: Option<&'a UptimeData>,
}
#[derive(Debug, Clone)]
pub struct UnifiedData {
pub title: String,
pub description: Option<String>,
pub struct UnifiedData<'a> {
pub title: Cow<'a, str>,
pub description: Option<Cow<'a, str>>,
pub auto_refresh_interval: u32,
pub groups: Vec<UnifiedGroupData>,
pub groups: Vec<UnifiedGroupData<'a>>,
}
#[derive(Debug, Clone)]
pub struct UnifiedGroupData {
pub group_info: StatusPageGroup,
pub monitors: Vec<UnifiedMonitorData>,
pub struct UnifiedGroupData<'a> {
pub group_info: &'a StatusPageGroup,
pub monitors: Vec<UnifiedMonitorData<'a>>,
}

View file

@ -13,7 +13,7 @@ pub struct HeartbeatEntry {
#[serde(skip)]
pub msg: String,
#[serde(default)]
pub ping: Option<u64>,
pub ping: Option<f32>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@ -55,7 +55,7 @@ impl HeartbeatResponse {
for (monitor_id_str, heartbeats) in &self.heartbeat_list_raw {
let monitor_id = monitor_id_str
.parse::<u64>()
.with_context(|| format!("{}", t("invalid-monitor-id")))?;
.with_context(|| t("invalid-monitor-id").to_string())?;
self.monitors.push(MonitorHeartbeats {
monitor_id,
@ -77,11 +77,11 @@ impl HeartbeatResponse {
let monitor_id = parts[0]
.parse::<u64>()
.with_context(|| format!("{}", t("invalid-monitor-id")))?;
.with_context(|| t("invalid-monitor-id").to_string())?;
let period_hours = parts[1]
.parse::<u32>()
.with_context(|| format!("{}", t("invalid-period-hours")))?;
.with_context(|| t("invalid-period-hours").to_string())?;
self.uptime_data.push(UptimeData {
monitor_id,

View file

@ -3,8 +3,8 @@ use crate::i18n::t;
use anyhow::{Context, Ok, Result};
pub fn parse_response(json_text: &str) -> Result<HeartbeatResponse> {
let mut response: HeartbeatResponse =
serde_json::from_str(json_text).with_context(|| t("invalid-json-heartbeat"))?;
let mut response: HeartbeatResponse = serde_json::from_slice(json_text.as_bytes())
.with_context(|| t("invalid-json-heartbeat"))?;
response.process()?;
Ok(response)

View file

@ -3,7 +3,7 @@ use crate::i18n::t;
use anyhow::{Context, Ok, Result};
pub fn parse_response(json_text: &str) -> Result<StatusPageResponse> {
let response: StatusPageResponse =
serde_json::from_str(json_text).with_context(|| t("invalid-json-status-page"))?;
let response: StatusPageResponse = serde_json::from_slice(json_text.as_bytes())
.with_context(|| t("invalid-json-status-page"))?;
Ok(response)
}

View file

@ -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();
@ -48,12 +48,8 @@ pub fn t(key: &str) -> String {
pub fn t_with_args(key: &str, args: &HashMap<&'static str, String>) -> String {
let mut map = HashMap::new();
let args_map: &HashMap<Cow<'static, _>, FluentValue<'_>>;
args_map = {
for (key, value) in args {
map.insert(Cow::Borrowed(*key), FluentValue::from(value.clone()));
}
&map
};
LOCALES.lookup_with_args(&get_system_locale(), key, args_map)
for (key, value) in args {
map.insert(Cow::Borrowed(*key), FluentValue::from(value));
}
LOCALES.lookup_with_args(get_system_locale(), key, &map)
}

View file

@ -6,9 +6,9 @@ Response = Response
response = response
invalid-json-status-page = ❌ Error parssing status page 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-monitor-id = Invalid monitor ID: {id}
invalid-period-hours = Invalid period in hours: {hours}
invalid-uptime-key-format = Invalid format for uptime key. Expected format "monitorID_period". Received key: {$key}
invalid-monitor-id = Invalid monitor ID: {$id}
invalid-period-hours = Invalid period in hours: {$hours}
loading = Loading...
monitors = Monitors
unknown = Unknown
@ -18,11 +18,11 @@ status = Status
error = Error
dashboard-header = Status Dashboard
never = Never
auto-update-failed = Automatic update failed: {error}
update-fail = Failed to update data: {error}
auto-update-failed = Automatic update failed: {$error}
update-fail = Failed to update data: {$error}
now = Now
uptime = Uptime
history = History
auto-update-enabled = Auto-update enabled ({interval} min)
update-failed = Update failed: {error}
auto-update-enabled = Auto-update enabled ({$interval} min)
update-failed = Update failed: {$error}
key-to-exit = Press 'q' to exit

View file

@ -5,10 +5,10 @@ missing_url = ❌ URL não fornecida. Use --url ou a variável de ambiente UPTIM
Response = Resposta
response = resposta
invalid-json-status-page = ❌ Falha ao parsear JSON do status page
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-monitor-id = ID de monitor inválido: {id}
invalid-period-hours = Período em horas inválido: {hours}
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-monitor-id = ID de monitor inválido: {$id}
invalid-period-hours = Período em horas inválido: {$hours}
loading = Carregando...
monitors = Monitors
monitor = Monitor
@ -23,6 +23,6 @@ auto-update-failed = Falha na atualização automática
update-fail = Falha ao atualizar dados
now = Agora
history = Historico
auto-update-enabled = Auto-atualização ativada ({interval} min)
update-failed = Falha na atualização: {error}
auto-update-enabled = Auto-atualização ativada ({$interval} min)
update-failed = Falha na atualização: {$error}
key-to-exit = Pressione 'q' para sair

View file

@ -1,12 +1,10 @@
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,
@ -18,23 +16,18 @@ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Padding, Paragraph},
widgets::{Block, Borders, Padding, Paragraph, Scrollbar, ScrollbarOrientation},
};
use std::{
collections::HashMap,
io,
sync::mpsc,
thread,
time::{Duration, Instant},
};
const INITIAL_INTERVAL: u32 = 300;
enum FetchResult {
Heartbeat(Result<HeartbeatResponse>),
StatusPage(Result<StatusPageResponse>),
}
const MAIN_LAYOUT_WITH_SCROLLBAR: [Constraint; 2] = [Constraint::Min(1), Constraint::Length(1)];
const MAIN_LAYOUT_WITHOUT_SCROLLBAR: [Constraint; 1] = [Constraint::Min(1)];
pub struct App {
state: DashboardViewState,
@ -47,6 +40,26 @@ pub struct 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<()> {
enable_raw_mode()?;
execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?;
@ -116,51 +129,101 @@ impl App {
fn render(&mut self) {
let _ = self.terminal.draw(|frame| {
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()
.direction(Direction::Vertical)
.margin(1)
.constraints([
Constraint::Length(3),
Constraint::Min(1),
Constraint::Length(3),
Constraint::Length(HEADER_HEIGHT),
Constraint::Length(max_content_height.max(1)),
Constraint::Length(FOOTER_HEIGHT),
])
.split(area);
render_header(frame, chunks[0], &self.state);
if self.state.is_loading {
Self::render_loading(frame, chunks[1]);
} 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]);
let main_constraint = if self.state.show_vertical_scrollbar(chunks[1].height) {
&MAIN_LAYOUT_WITH_SCROLLBAR[..]
} 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
.update_interval
.checked_sub(self.last_update.elapsed())
.map(|d| d.as_secs() as u64)
.map(|d| d.as_secs())
.unwrap_or(30);
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<()> {
let timeout = Duration::from_secs(1);
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Release {
return Ok(());
}
if event::poll(timeout)?
&& let Event::Key(key) = event::read()?
{
if key.kind == KeyEventKind::Release {
return Ok(());
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
_ => {}
}
match key.code {
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(())
@ -180,7 +243,6 @@ impl App {
fn refresh_data(&mut self) -> io::Result<()> {
self.state.is_loading = true;
self.render();
match self.fetch_and_update_data() {
Ok(()) => {
@ -197,57 +259,11 @@ impl App {
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<()> {
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 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);
let unified_data = core::unify_data(&status_page_result, &heartbeat_result);
self.state = DashboardViewState::from_unified_data(unified_data);
Ok(())
}

View file

@ -9,7 +9,11 @@ use ratatui::{
use crate::i18n::t;
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 datatime_str = now.format("%Y-%m-%d %H:%M:%S").to_string();
let countdown_str = format!("{}s", seconds_until_update);

View file

@ -1,10 +1,19 @@
use std::cmp::min;
use crate::i18n::t;
use crate::ui::dashboard::{
MonitorStatus, MonitorViewState,
model::{DashboardViewState, GroupViewState},
use std::{
borrow::Cow,
cmp::min,
collections::{HashMap, hash_map::DefaultHasher},
hash::{Hash, Hasher},
sync::{OnceLock, RwLock},
};
use crate::{
i18n::t,
ui::dashboard::{
MonitorStatus, MonitorViewState,
model::{BORDER_LINES_VIEW, DashboardViewState, GroupViewState},
},
};
use once_cell::sync::Lazy;
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
@ -16,50 +25,72 @@ use ratatui::{
const STATUS_LINE_LENGTH: usize = 100;
const MAX_NAME_LENGTH: usize = 30;
pub fn render_monitor_list(frame: &mut Frame, area: Rect, state: &DashboardViewState) {
let group_areas = layout_groups(area, &state.groups);
static UP_SPAN: Lazy<Span<'static>> =
Lazy::new(|| Span::styled("", Style::default().fg(Color::Green)));
for (i, (group, &group_area)) in state.groups.iter().zip(group_areas.iter()).enumerate() {
render_group(frame, group_area, group, i == 0);
}
}
static DOWN_SPAN: Lazy<Span<'static>> =
Lazy::new(|| Span::styled("", Style::default().fg(Color::Red)));
fn layout_groups(area: Rect, groups: &[GroupViewState]) -> Vec<Rect> {
let total_lines: usize = groups.iter().map(|g| g.monitors.len() + 1).sum();
static UNKNOWN_SPAN: Lazy<Span<'static>> =
Lazy::new(|| Span::styled("", Style::default().fg(Color::Yellow)));
if total_lines == 0 {
return Vec::new();
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);
}
let content_height = area.height.saturating_sub(2);
let line_height = content_height as usize / total_lines;
let scroll_pos = state.scroll_state.get_position();
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 mut current_y = area.y + 1;
let mut areas = Vec::with_capacity(groups.len());
for group in state.groups.iter() {
let group_height = group.monitors.len() + BORDER_LINES_VIEW;
for group in groups {
let group_lines = group.monitors.len() + 2;
let group_height = (group_lines + line_height).max(1);
areas.push(Rect {
if lines_skipped + group_height <= scroll_pos {
lines_skipped += group_height;
continue;
}
let visible_height = if lines_skipped < scroll_pos {
group_height - (scroll_pos - lines_skipped)
} else {
group_height
}
.min(available_height - rendered_height);
let group_area = Rect {
x: area.x,
y: current_y,
y: current_y as u16,
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: bool) {
fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first_half: bool) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(0)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.split(area);
if chunks[0].height == 0 || chunks[1].height == 0 || group.monitors.is_empty() {
return;
}
let group_title = Line::from(vec![
Span::styled(format!("{} ", group.name), title_style()),
Span::styled(
@ -69,11 +100,7 @@ fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first:
]);
let title_block = Block::default()
.borders(if is_first {
Borders::TOP | Borders::LEFT | Borders::RIGHT
} else {
Borders::ALL
})
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Blue))
.title(group_title)
.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]);
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 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![
"".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 rows: Vec<Row> = monitors
.iter()
.take(items_to_show)
.map(|monitor| create_monitor_item(monitor))
.collect();
let mut rows: Vec<Row> = Vec::with_capacity(items_to_show);
for monitor in monitors.iter().take(items_to_show) {
rows.push(create_monitor_item(monitor));
}
let widths = vec![
Constraint::Length(3),
@ -127,14 +164,6 @@ fn render_monitor_table(frame: &mut Frame, area: Rect, monitors: &Vec<MonitorVie
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 {
match status {
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 {
MonitorStatus::Up => "",
MonitorStatus::Down => "",
MonitorStatus::Unknown => "",
MonitorStatus::Up => Cow::Borrowed(UP_ICON),
MonitorStatus::Down => Cow::Borrowed(DOWN_ICON),
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_color = get_status_color(&monitor.status);
let display_name: String = if monitor.name.len() > MAX_NAME_LENGTH {
format!("{:.width$}...", &monitor.name, width = MAX_NAME_LENGTH - 3)
let display_name: Cow<str> = if monitor.name.len() > MAX_NAME_LENGTH {
Cow::Owned(format!(
"{:.width$}...",
monitor.name,
width = MAX_NAME_LENGTH - 3
))
} else {
monitor.name.clone()
Cow::Borrowed(&monitor.name)
};
let response_text = format!("{:>7}ms", monitor.response_time);
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![
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(uptime_text, Color::Magenta, Modifier::empty()),
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<'_> {
let recent_status: Vec<_> = status_history
.iter()
.rev()
.take(STATUS_LINE_LENGTH)
.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)));
fn get_status_span(status: &MonitorStatus) -> &'static Span<'static> {
match status {
MonitorStatus::Up => &UP_SPAN,
MonitorStatus::Down => &DOWN_SPAN,
MonitorStatus::Unknown => &UNKNOWN_SPAN,
}
Line::from(spans)
}
fn title_style() -> Style {
@ -207,3 +233,46 @@ fn title_style() -> Style {
.fg(Color::Yellow)
.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
}

View file

@ -1,9 +1,12 @@
use crate::core::models::{UnifiedData, UnifiedGroupData};
use crate::data::heartbeat::model::HeartbeatEntry;
use crate::i18n::t;
use ratatui::widgets::ScrollbarState;
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 {
Up,
Down,
@ -12,7 +15,7 @@ pub enum MonitorStatus {
#[derive(Debug, Clone)]
pub struct MonitorViewState {
pub name: String,
pub name: Cow<'static, str>,
pub status: MonitorStatus,
pub response_time: String,
pub uptime_24h: String,
@ -33,6 +36,8 @@ pub struct DashboardViewState {
pub is_loading: bool,
pub error_message: Option<String>,
pub auto_refresh_interval: u32,
pub scroll_state: ScrollbarState,
total_length: usize,
}
impl DashboardViewState {
@ -44,6 +49,8 @@ impl DashboardViewState {
is_loading: true,
error_message: None,
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 {
title: data.title,
descriptions: data.description,
title: data.title.into_owned(),
descriptions: data.description.map(|d| d.into_owned()),
groups,
is_loading: false,
error_message: None,
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> {
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> {
let mut history = heartbeats
.iter()
.rev()
.take(100)
.map(|h| match h.status {
0 => MonitorStatus::Down,
1 => MonitorStatus::Up,
_ => MonitorStatus::Unknown,
})
.collect::<Vec<_>>();
let mut history: Vec<_> = Vec::with_capacity(heartbeats.len());
history.extend(heartbeats.iter().rev().take(100).map(|h| match h.status {
0 => MonitorStatus::Down,
1 => MonitorStatus::Up,
_ => MonitorStatus::Unknown,
}));
while history.len() < 100 {
history.push(MonitorStatus::Unknown);
@ -94,36 +112,42 @@ fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec<MonitorStatus> {
fn add_monitor_view_state(group: UnifiedGroupData) -> Vec<MonitorViewState> {
let mut monitors = Vec::with_capacity(group.monitors.len());
group.monitors.into_par_iter().map(|monitor| {
group
.monitors
.into_par_iter()
.map(|monitor| {
let status_history = get_status_history(monitor.heartbeats);
let status_history = get_status_history(&monitor.heartbeats);
let status = match monitor.heartbeats.last().map(|h| h.status) {
Some(1) => MonitorStatus::Up,
Some(0) => MonitorStatus::Down,
_ => MonitorStatus::Unknown,
};
let status = match monitor.heartbeats.last().map(|h| h.status) {
Some(1) => MonitorStatus::Up,
Some(0) => MonitorStatus::Down,
_ => MonitorStatus::Unknown,
};
let response_time = match monitor.heartbeats.last().and_then(|h| h.ping) {
Some(ms) => format!("{}", ms),
None => t("unknown"),
};
let response_time = monitor
.heartbeats
.last()
.and_then(|h| h.ping)
.map(|ms| format!("{}", ms))
.unwrap_or_else(|| t("unknown").to_string() + " ");
let uptime_24h = match monitor.uptime_data.map(|u| u.get_perc_formated()) {
Some(perc) => perc,
None => t("unknown"),
};
let uptime_24h = monitor
.uptime_data
.map(|u| u.get_perc_formated())
.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 {
name: monitor.name,
status,
response_time,
uptime_24h,
status_history,
}
}).collect_into_vec(&mut monitors);
MonitorViewState {
name,
status,
response_time,
uptime_24h,
status_history,
}
})
.collect_into_vec(&mut monitors);
monitors.sort_by_key(|m| m.name.clone());