Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc06c06cac | ||
|
|
bc86364e68 | ||
|
|
663ca64cd7 | ||
|
|
330b70752e | ||
|
|
0c9ad61a4d | ||
|
|
38bf187a4a | ||
|
|
2c120ffaff | ||
|
|
572ca8858a | ||
|
|
2c4f2a1e05 | ||
|
|
b4ef92fdcc | ||
|
|
6d1b5d339a | ||
|
|
50e2124d07 | ||
|
|
3eb61969b3 | ||
|
|
b87ed202ea | ||
|
|
b5ebb33627 | ||
|
|
d67ffbf387 | ||
|
|
b0eff24087 | ||
|
|
1ba67c8af0 | ||
|
|
2f195f611c | ||
|
|
e7673c04db | ||
|
|
7292ed3d1b | ||
|
|
def2efa0d1 | ||
|
|
21c110011f |
47
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: CI
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: "main"
|
||||
|
||||
jobs:
|
||||
build-binary:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
|
||||
- name: Install deps
|
||||
run: cd web && bun install
|
||||
|
||||
- name: Build
|
||||
run: ./build.sh cargo build --verbose
|
||||
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
75
.github/workflows/nightly.yml
vendored
@@ -1,46 +1,48 @@
|
||||
name: Nightly Release
|
||||
on:
|
||||
push:
|
||||
branches: main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-binary:
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- release_for: linux-aarch64
|
||||
os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
bin: cup
|
||||
name: cup-linux-aarch64
|
||||
command: build
|
||||
|
||||
- release_for: linux-x86_64
|
||||
os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
bin: cup
|
||||
name: cup-linux-x86_64
|
||||
command: build
|
||||
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
build-binaries:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup rust
|
||||
- name: Set up Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install cross
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Build binary
|
||||
run: cross ${{ matrix.platform.command }} --target ${{ matrix.platform.target }}
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
|
||||
- name: Upload CLI
|
||||
- name: Install deps
|
||||
run: cd web && bun install
|
||||
|
||||
- name: Build amd64 binary
|
||||
run: |
|
||||
./build.sh cross build --target x86_64-unknown-linux-musl --release
|
||||
mv target/x86_64-unknown-linux-musl/release/cup ./cup-linux-amd64
|
||||
|
||||
- name: Build arm64 binary
|
||||
run: |
|
||||
./build.sh cross build --target aarch64-unknown-linux-musl --release
|
||||
mv target/aarch64-unknown-linux-musl/release/cup ./cup-linux-arm64
|
||||
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.platform.name }}
|
||||
path: target/${{ matrix.platform.target }}/debug/${{ matrix.platform.bin }}
|
||||
name: binaries
|
||||
path: |
|
||||
cup-linux-amd64
|
||||
cup-linux-arm64
|
||||
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -69,4 +71,21 @@ jobs:
|
||||
push: true
|
||||
tags: ghcr.io/sergi0g/cup:nightly
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
nightly-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-binaries
|
||||
steps:
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: binaries
|
||||
path: binaries
|
||||
|
||||
- uses: pyTooling/Actions/releaser@r0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: nightly
|
||||
rm: true
|
||||
files: binaries/*
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags: ["v*.*.*"]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
get-tag:
|
||||
@@ -9,37 +8,56 @@ jobs:
|
||||
outputs:
|
||||
tag: ${{ steps.tag.outputs.tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get current tag
|
||||
id: tag
|
||||
run: |
|
||||
TAG=$(echo "$GITHUB_REF" | sed 's/^refs\/tags\///')
|
||||
TAG=v$(head -n 4 Cargo.toml | grep version | awk '{print $3}' | tr -d '"')
|
||||
echo "Current tag: $TAG"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
build-binary:
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- aarch64
|
||||
- x86_64
|
||||
|
||||
build-binaries:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup rust
|
||||
- name: Set up Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install cross
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Build binary
|
||||
run: cross build --target ${{ matrix.arch }}-unknown-linux-musl --release
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Upload binary
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
|
||||
- name: Install deps
|
||||
run: cd web && bun install
|
||||
|
||||
- name: Build amd64 binary
|
||||
run: |
|
||||
./build.sh cross build --target x86_64-unknown-linux-musl --release
|
||||
mv target/x86_64-unknown-linux-musl/release/cup ./cup-linux-amd64
|
||||
|
||||
- name: Build arm64 binary
|
||||
run: |
|
||||
./build.sh cross build --target aarch64-unknown-linux-musl --release
|
||||
mv target/aarch64-unknown-linux-musl/release/cup ./cup-linux-arm64
|
||||
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cup-linux-${{ matrix.arch }}
|
||||
path: target/${{ matrix.arch }}-unknown-linux-musl/release/cup
|
||||
name: binaries
|
||||
path: |
|
||||
cup-linux-amd64
|
||||
cup-linux-arm64
|
||||
|
||||
build-image:
|
||||
needs: get-tag
|
||||
@@ -73,24 +91,13 @@ jobs:
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-tag, build-image, build-binary]
|
||||
needs: [get-tag, build-image, build-binaries]
|
||||
steps:
|
||||
- name: Download arm64 binary
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cup-linux-aarch64
|
||||
path: cup-linux-aarch64
|
||||
|
||||
- name: Download x86 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cup-linux-x86_64
|
||||
path: cup-linux-x86_64
|
||||
|
||||
# - name: Extract and rename binaries
|
||||
# run: |
|
||||
# unzip /home/runner/work/cup/cup/cup-linux-aarch64 && mv /home/runner/work/cup/cup/cup cup-linux-aarch64
|
||||
# unzip /home/runner/work/cup/cup/cup-linux-x86_64 && mv /home/runner/work/cup/cup/cup cup-linux-x86_64
|
||||
name: binaries
|
||||
path: binaries
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
@@ -100,6 +107,4 @@ jobs:
|
||||
prerelease: true
|
||||
tag_name: ${{ needs.get-tag.outputs.tag }}
|
||||
name: ${{ needs.get-tag.outputs.tag }}
|
||||
files: |
|
||||
cup-linux-aarch64
|
||||
cup-linux-x86_64
|
||||
files: binaries/*
|
||||
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
/docs/.next
|
||||
/docs/node_modules
|
||||
/docs/out
|
||||
/src/static
|
||||
|
||||
# In case I accidentally commit mine...
|
||||
cup.json
|
||||
@@ -1 +0,0 @@
|
||||
rust 1.79.0
|
||||
614
Cargo.lock
generated
@@ -43,9 +43,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.14"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
|
||||
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
@@ -64,38 +64,55 @@ checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
|
||||
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
|
||||
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.3"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
|
||||
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
|
||||
|
||||
[[package]]
|
||||
name = "anymap2"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
@@ -128,18 +145,18 @@ dependencies = [
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -155,7 +172,7 @@ version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0aed08d3adb6ebe0eff737115056652670ae290f177759aac19c30456135f94c"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"base64",
|
||||
"bollard-stubs",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -199,6 +216,12 @@ version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.6.0"
|
||||
@@ -272,9 +295,9 @@ checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
@@ -304,40 +327,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@@ -350,21 +339,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cup"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
dependencies = [
|
||||
"bollard",
|
||||
"chrono",
|
||||
"clap",
|
||||
"futures",
|
||||
"http-auth",
|
||||
"indicatif",
|
||||
"json",
|
||||
"liquid",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
"reqwest-retry",
|
||||
"termsize",
|
||||
"tokio",
|
||||
"ureq",
|
||||
"xitca-web",
|
||||
]
|
||||
|
||||
@@ -412,16 +403,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -437,6 +418,21 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.30"
|
||||
@@ -444,6 +440,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -452,6 +449,23 @@ version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.30"
|
||||
@@ -481,9 +495,13 @@ version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
@@ -506,8 +524,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -572,13 +592,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "643c9bbf6a4ea8a656d6b4cd53d34f79e3f841ad5203c1a55fb7d761923bc255"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"digest",
|
||||
"hex",
|
||||
"md-5",
|
||||
"memchr",
|
||||
"rand",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -650,6 +664,24 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.6"
|
||||
@@ -760,19 +792,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
name = "ipnet"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||
checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
@@ -800,9 +841,9 @@ checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd"
|
||||
|
||||
[[package]]
|
||||
name = "kstring"
|
||||
version = "2.0.0"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747"
|
||||
checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"static_assertions",
|
||||
@@ -822,9 +863,9 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "liquid"
|
||||
version = "0.26.6"
|
||||
version = "0.26.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10929f201279ba14da3297b957dcda1e0bf7a6f3bb5115688be684aa8864e9cc"
|
||||
checksum = "7cdcc72b82748f47c2933c172313f5a9aea5b2c4eb3fa4c66b4ea55bb60bb4b1"
|
||||
dependencies = [
|
||||
"doc-comment",
|
||||
"liquid-core",
|
||||
@@ -835,9 +876,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "liquid-core"
|
||||
version = "0.26.6"
|
||||
version = "0.26.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3aef4b2160791f456eb880c990a97746f693746f92302ef5f1d06111cf14b768"
|
||||
checksum = "2752e978ffc53670f3f2e8b3ef09f348d6f7b5474a3be3f8a5befe5382e4effb"
|
||||
dependencies = [
|
||||
"anymap2",
|
||||
"itertools",
|
||||
@@ -853,9 +894,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "liquid-derive"
|
||||
version = "0.26.5"
|
||||
version = "0.26.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "915f6d0a2963a27cd5205c1902f32ddfe3bc035816afd268cf88c0fc0f8d287e"
|
||||
checksum = "3b51f1d220e3fa869e24cfd75915efe3164bd09bb11b3165db3f37f57bf673e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -864,9 +905,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "liquid-lib"
|
||||
version = "0.26.6"
|
||||
version = "0.26.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f48fc446873f74d869582f5c4b8cbf3248c93395e410a67af5809b3731e44a"
|
||||
checksum = "59b1a298d3d2287ee5b1e43840d885b8fdfc37d3f4e90d82aacfd04d021618da"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"liquid-core",
|
||||
@@ -877,28 +918,34 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.4"
|
||||
@@ -981,6 +1028,31 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
@@ -989,9 +1061,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.7.11"
|
||||
version = "2.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95"
|
||||
checksum = "9c73c26c01b8c87956cea613c907c9d6ecffd8d18a2a5908e5de0adfaa185cea"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror",
|
||||
@@ -1000,9 +1072,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.7.11"
|
||||
version = "2.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a"
|
||||
checksum = "664d22978e2815783adbdd2c588b455b1bd625299ce36b2a99881ac9627e6d8d"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
@@ -1010,9 +1082,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.7.11"
|
||||
version = "2.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183"
|
||||
checksum = "a2d5487022d5d33f4c30d91c22afa240ce2a644e87fe08caad974d4eab6badbe"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
@@ -1023,9 +1095,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.7.11"
|
||||
version = "2.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f"
|
||||
checksum = "0091754bbd0ea592c4deb3a122ce8ecbb0753b738aa82bc055fcc2eccc8d8174"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"pest",
|
||||
@@ -1078,9 +1150,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
@@ -1091,6 +1166,54 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"rand",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
@@ -1131,23 +1254,12 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.10.0"
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1179,6 +1291,93 @@ version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-middleware"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562ceb5a604d3f7c885a792d42c199fd8af239d0a51b2fa6a78aafa092452b04"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"http",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-retry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a83df1aaec00176d0fabb65dea13f832d2a446ca99107afc17c5d2d4981221d0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"futures",
|
||||
"getrandom",
|
||||
"http",
|
||||
"hyper",
|
||||
"parking_lot",
|
||||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
"retry-policies",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"wasm-timer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "retry-policies"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5875471e6cab2871bc150ecb8c727db5113c9338cc3354dc5ee3425b6aa40a1c"
|
||||
dependencies = [
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
@@ -1201,12 +1400,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.10"
|
||||
name = "rustc-hash"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402"
|
||||
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -1216,16 +1420,26 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.7.0"
|
||||
name = "rustls-pemfile"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
|
||||
checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.5"
|
||||
version = "0.102.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78"
|
||||
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -1238,6 +1452,12 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.204"
|
||||
@@ -1298,7 +1518,7 @@ version = "3.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e73139bc5ec2d45e6c5fd85be5a46949c1c39a4c18e56915f5eb4c12f975e377"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"base64",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
@@ -1398,6 +1618,15 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termsize"
|
||||
version = "0.1.8"
|
||||
@@ -1514,6 +1743,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.11"
|
||||
@@ -1652,9 +1892,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.11.0"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
@@ -1668,22 +1908,6 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"flate2",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"url",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.2"
|
||||
@@ -1753,6 +1977,18 @@ dependencies = [
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.92"
|
||||
@@ -1783,10 +2019,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.3"
|
||||
name = "wasm-timer"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd"
|
||||
checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"js-sys",
|
||||
"parking_lot",
|
||||
"pin-utils",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -1822,6 +2083,36 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
@@ -2048,6 +2339,27 @@ dependencies = [
|
||||
"xitca-unsafe-collection",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
|
||||
12
Cargo.toml
@@ -1,23 +1,25 @@
|
||||
[package]
|
||||
name = "cup"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.7", features = ["derive"] }
|
||||
indicatif = { version = "0.17.8", optional = true }
|
||||
tokio = {version = "1.38.0", features = ["rt", "rt-multi-thread", "macros"]}
|
||||
ureq = { version = "2.9.7", features = ["tls"] }
|
||||
rayon = "1.10.0"
|
||||
tokio = {version = "1.38.0", features = ["macros"]}
|
||||
xitca-web = { version = "0.5.0", optional = true, features = ["logger"] }
|
||||
liquid = { version = "0.26.6", optional = true }
|
||||
bollard = "0.16.1"
|
||||
once_cell = "1.19.0"
|
||||
http-auth = { version = "0.1.9", features = [] }
|
||||
http-auth = { version = "0.1.9", default-features = false, features = [] }
|
||||
termsize = { version = "0.1.8", optional = true }
|
||||
regex = "1.10.5"
|
||||
chrono = { version = "0.4.38", default-features = false, features = ["std", "alloc", "clock"], optional = true }
|
||||
json = "0.12.4"
|
||||
reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls"] }
|
||||
futures = "0.3.30"
|
||||
reqwest-retry = "0.6.1"
|
||||
reqwest-middleware = "0.3.3"
|
||||
|
||||
[features]
|
||||
default = ["server", "cli"]
|
||||
|
||||
40
Dockerfile
@@ -1,20 +1,42 @@
|
||||
FROM rust:alpine AS build
|
||||
WORKDIR /
|
||||
### Build UI ###
|
||||
FROM node:20 AS web
|
||||
|
||||
# Install bun
|
||||
RUN curl -fsSL https://bun.sh/install | bash
|
||||
|
||||
# Copy web folder
|
||||
COPY ./web /web
|
||||
WORKDIR /web
|
||||
|
||||
# Install requirements
|
||||
RUN ~/.bun/bin/bun install
|
||||
|
||||
# Build frontend
|
||||
RUN ~/.bun/bin/bun run build
|
||||
|
||||
### Build Cup ###
|
||||
FROM rust:1.80.1-alpine AS build
|
||||
|
||||
# Requirements
|
||||
RUN apk add musl-dev
|
||||
|
||||
RUN USER=root cargo new --bin cup
|
||||
# Copy files
|
||||
WORKDIR /cup
|
||||
|
||||
COPY Cargo.toml Cargo.lock .
|
||||
RUN cargo build --release
|
||||
RUN rm -rf src/
|
||||
COPY Cargo.toml .
|
||||
COPY Cargo.lock .
|
||||
COPY ./src ./src
|
||||
|
||||
COPY src src
|
||||
# This is a very bad workaround, but cargo only triggers a rebuild this way for some reason
|
||||
RUN printf "\n" >> src/main.rs
|
||||
# Copy UI from web builder
|
||||
COPY --from=web /web/dist src/static
|
||||
|
||||
# Build
|
||||
RUN cargo build --release
|
||||
|
||||
### Main ###
|
||||
FROM scratch
|
||||
|
||||
# Copy binary
|
||||
COPY --from=build /cup/target/release/cup /cup
|
||||
|
||||
ENTRYPOINT ["/cup"]
|
||||
@@ -11,7 +11,7 @@ Cup is the easiest way to check for container image updates.
|
||||
|
||||
## Features
|
||||
|
||||
- Extremely fast. Cup takes full advantage of your CPU and is hightly optimized, resulting in lightning fast speed. On my test machine, it took ~6 seconds for 70 images.
|
||||
- Extremely fast. Cup takes full advantage of your CPU and is hightly optimized, resulting in lightning fast speed. On my test machine, it took ~12 seconds for ~95 images.
|
||||
- Supports most registries, including Docker Hub, ghcr.io, Quay, lscr.io and even Gitea (or derivatives)
|
||||
- Doesn't exhaust any rate limits. This is the original reason I created Cup. It was inspired by [What's up docker?](https://github.com/fmartinou/whats-up-docker) which would always use it up.
|
||||
- Beautiful CLI and web interface for checking on your containers any time.
|
||||
@@ -27,7 +27,7 @@ Take a look at https://sergi0g.github.io/cup/docs!
|
||||
Cup is a work in progress. It might not have as many features as What's up Docker. If one of these features is really important for you, please consider using another tool.
|
||||
|
||||
- ~~Cup currently doesn't support registries which use repositories without slashes. This includes Azure. This problem may sound a bit weird, but it's due to the regex that's used at the moment. This will (hopefully) be fixed in the future.~~
|
||||
- Cup doesn't support private images. This is on the roadmap. Currently, it just returns unknown for those images.
|
||||
- ~~Cup doesn't support private images. This is on the roadmap. Currently, it just returns unknown for those images.~~
|
||||
- Cup cannot trigger your integrations. If you want that to happen automatically, please use What's up docker instead. Cup was created to be simple. The data is there, and it's up to you to retrieve it (e.g. by running `cup check -r` with a cronjob or periodically requesting the `/json` url from the server)
|
||||
|
||||
## Roadmap
|
||||
@@ -46,8 +46,6 @@ Here are some ideas to get you started:
|
||||
|
||||
To contribute, fork the repository, make your changes and the submit a pull request.
|
||||
|
||||
Note: If you update the UI, please make sure to recompile the CSS with `tailwindcss -mo src/static/index.css`. You need to have the Tailwind CSS CLI installed ([instructions here](https://tailwindcss.com/docs/installation))
|
||||
|
||||
## Support
|
||||
|
||||
If you have any questions about Cup, feel free to ask in the [discussions](https://github.com/sergi0g/cup/discussions)!
|
||||
|
||||
22
build.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Exit on error
|
||||
set -e
|
||||
|
||||
# This is kind of like a shim that makes sure the frontend is rebuilt when running a build. For example you can run `./build.sh cargo build --release`
|
||||
|
||||
# Remove old files
|
||||
rm -rf src/static
|
||||
|
||||
# Frontend
|
||||
cd web/
|
||||
|
||||
# Build
|
||||
bun run build
|
||||
|
||||
# Copy UI to src folder
|
||||
cp -r dist/ ../src/static
|
||||
|
||||
# Run command from argv
|
||||
|
||||
$@
|
||||
BIN
docs/assets/350767810-42eccc89-bdfd-426a-a113-653abe7483d8.png
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
docs/assets/358304960-e9f26767-51f7-4b5a-8b74-a5811019497b.jpeg
Normal file
|
After Width: | Height: | Size: 107 KiB |
@@ -11,6 +11,9 @@
|
||||
"usage": {
|
||||
"title": "Usage"
|
||||
},
|
||||
"community-resources": {
|
||||
"title": "Community Resources"
|
||||
},
|
||||
"nightly": {
|
||||
"title": "Using the latest version"
|
||||
}
|
||||
|
||||
23
docs/pages/docs/community-resources/docker-compose.mdx
Normal file
@@ -0,0 +1,23 @@
|
||||
# Docker Compose
|
||||
|
||||
Many users find it useful to run Cup with Docker Compose, as it enables them to have it constantly running in the background and easily control it. Cup's lightweight resource usae makes it ideal for this use case.
|
||||
|
||||
There have been requests for an official Docker Compose file, but I believe you should customize it to your needs.
|
||||
|
||||
Here is an example of what I would use (by [@ioverho](https://github.com/ioverho)):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
cup:
|
||||
image: ghcr.io/sergi0g/cup:latest
|
||||
container_name: cup # Optional
|
||||
restart: unless-stopped
|
||||
command: -c /config/cup.json serve
|
||||
ports:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./cup.json:/config/cup.json
|
||||
```
|
||||
|
||||
This can be customized further of course, if you choose to use a different port, another config location, or would like to change something else. Have fun!
|
||||
75
docs/pages/docs/community-resources/homepage-widget.mdx
Normal file
@@ -0,0 +1,75 @@
|
||||
import Image from 'next/image';
|
||||
import widget1 from '../../../assets/350767810-42eccc89-bdfd-426a-a113-653abe7483d8.png'
|
||||
import widget2 from '../../../assets/358304960-e9f26767-51f7-4b5a-8b74-a5811019497b.jpeg'
|
||||
|
||||
# Homepage Widget
|
||||
|
||||
Some users have asked for a homepage widget.
|
||||
|
||||
## Docker Compose with the widget configured via labels:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
cup:
|
||||
image: ghcr.io/sergi0g/cup
|
||||
container_name: cup
|
||||
command: -c /config/cup.json serve -p 8000
|
||||
volumes:
|
||||
- ./config/cup.json:/config/cup.json
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 8000:8000
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
homepage.group: Network
|
||||
homepage.name: Cup
|
||||
homepage.icon: /icons/cup-with-straw.png
|
||||
homepage.href: http://myserver:8000
|
||||
homepage.ping: http://myserver:8000
|
||||
homepage.description: Checks for container updates
|
||||
homepage.widget.type: customapi
|
||||
homepage.widget.url: http://myserver:8000/json
|
||||
homepage.widget.mappings[0].label: Monitoring
|
||||
homepage.widget.mappings[0].field.metrics: monitored_images
|
||||
homepage.widget.mappings[0].format: number
|
||||
homepage.widget.mappings[1].label: Up to date
|
||||
homepage.widget.mappings[1].field.metrics: up_to_date
|
||||
homepage.widget.mappings[1].format: number
|
||||
homepage.widget.mappings[2].label: Updates
|
||||
homepage.widget.mappings[2].field.metrics: update_available
|
||||
homepage.widget.mappings[2].format: number
|
||||
```
|
||||
Preview:
|
||||
<Image src={widget1}/>
|
||||
|
||||
Credit: [@agrmohit](https://github.com/agrmohit)
|
||||
|
||||
## Widget in Homepage's config file format:
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: customapi
|
||||
url: http://<SERVER_IP>:9000/json
|
||||
refreshInterval: 10000
|
||||
method: GET
|
||||
mappings:
|
||||
- field:
|
||||
metrics: monitored_images
|
||||
label: Monitored images
|
||||
format: number
|
||||
- field:
|
||||
metrics: up_to_date
|
||||
label: Up to date
|
||||
format: number
|
||||
- field:
|
||||
metrics: update_available
|
||||
label: Available updates
|
||||
format: number
|
||||
- field:
|
||||
metrics: unknown
|
||||
label: Unknown
|
||||
format: number
|
||||
```
|
||||
Preview:
|
||||
<Image src={widget2}/>
|
||||
Credit: [@remussamoila](https://github.com/remussamoila)
|
||||
@@ -16,7 +16,7 @@ export default {
|
||||
const { asPath } = useRouter()
|
||||
const { frontMatter } = useConfig()
|
||||
const url =
|
||||
'https://sergi0g.github.io/cup' +
|
||||
'https://sergi0g.github.io/cup/docs/' +
|
||||
(`/${asPath}`);
|
||||
|
||||
return (
|
||||
@@ -36,9 +36,9 @@ export default {
|
||||
<h1 className="font-bold ml-2">Cup</h1>
|
||||
</div>
|
||||
),
|
||||
logoLink: "sergi0g.github.io/cup",
|
||||
logoLink: "https://sergi0g.github.io/cup/docs/",
|
||||
project: {
|
||||
link: "https://github.com/sergi0g/cup",
|
||||
link: "https://github.com/sergi0g/cup/",
|
||||
},
|
||||
navbar: {
|
||||
extraContent: <ThemeSwitch lite className="[&_span]:hidden" />,
|
||||
|
||||
94
src/check.rs
@@ -1,9 +1,12 @@
|
||||
use std::{collections::{HashMap, HashSet}, sync::Mutex};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
use json::JsonValue;
|
||||
|
||||
use crate::{docker::get_images_from_docker_daemon, image::Image, registry::{check_auth, get_token, get_latest_digests}, utils::unsplit_image};
|
||||
use crate::{
|
||||
debug,
|
||||
docker::get_images_from_docker_daemon,
|
||||
image::Image,
|
||||
registry::{check_auth, get_latest_digests, get_token},
|
||||
utils::{new_reqwest_client, unsplit_image, CliConfig},
|
||||
};
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
use crate::docker::get_image_from_docker_daemon;
|
||||
@@ -25,64 +28,79 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_all_updates(socket: Option<String>, config: &JsonValue) -> Vec<(String, Option<bool>)> {
|
||||
let image_map_mutex: Mutex<HashMap<String, &Option<String>>> = Mutex::new(HashMap::new());
|
||||
let local_images = get_images_from_docker_daemon(socket).await;
|
||||
local_images.par_iter().for_each(|image| {
|
||||
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
|
||||
image_map_mutex.lock().unwrap().insert(img, &image.digest);
|
||||
});
|
||||
let image_map = image_map_mutex.lock().unwrap().clone();
|
||||
let mut registries: Vec<&String> = local_images
|
||||
.par_iter()
|
||||
.map(|image| &image.registry)
|
||||
.collect();
|
||||
pub async fn get_all_updates(options: &CliConfig) -> Vec<(String, Option<bool>)> {
|
||||
let local_images = get_images_from_docker_daemon(options).await;
|
||||
let mut image_map: HashMap<String, Option<String>> = HashMap::with_capacity(local_images.len());
|
||||
for image in &local_images {
|
||||
let img = unsplit_image(image);
|
||||
image_map.insert(img, image.digest.clone());
|
||||
}
|
||||
let mut registries: Vec<&String> = local_images.iter().map(|image| &image.registry).collect();
|
||||
registries.unique();
|
||||
let mut remote_images: Vec<Image> = Vec::new();
|
||||
let mut remote_images: Vec<Image> = Vec::with_capacity(local_images.len());
|
||||
let client = new_reqwest_client();
|
||||
for registry in registries {
|
||||
if options.verbose {
|
||||
debug!("Checking images from registry {}", registry)
|
||||
}
|
||||
let images: Vec<&Image> = local_images
|
||||
.par_iter()
|
||||
.iter()
|
||||
.filter(|image| &image.registry == registry)
|
||||
.collect();
|
||||
let credentials = config["authentication"][registry].clone().take_string().or(None);
|
||||
let mut latest_images = match check_auth(registry, config) {
|
||||
let credentials = options.config["authentication"][registry]
|
||||
.clone()
|
||||
.take_string()
|
||||
.or(None);
|
||||
let mut latest_images = match check_auth(registry, options, &client).await {
|
||||
Some(auth_url) => {
|
||||
let token = get_token(images.clone(), &auth_url, &credentials);
|
||||
get_latest_digests(images, Some(&token), config)
|
||||
let token = get_token(images.clone(), &auth_url, &credentials, &client).await;
|
||||
if options.verbose {
|
||||
debug!("Using token {}", token);
|
||||
}
|
||||
get_latest_digests(images, Some(&token), options, &client).await
|
||||
}
|
||||
None => get_latest_digests(images, None, config),
|
||||
None => get_latest_digests(images, None, options, &client).await,
|
||||
};
|
||||
remote_images.append(&mut latest_images);
|
||||
}
|
||||
let result_mutex: Mutex<Vec<(String, Option<bool>)>> = Mutex::new(Vec::new());
|
||||
remote_images.par_iter().for_each(|image| {
|
||||
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
|
||||
if options.verbose {
|
||||
debug!("Collecting results")
|
||||
}
|
||||
let mut result: Vec<(String, Option<bool>)> = Vec::new();
|
||||
remote_images.iter().for_each(|image| {
|
||||
let img = unsplit_image(image);
|
||||
match &image.digest {
|
||||
Some(d) => {
|
||||
let r = d != image_map.get(&img).unwrap().as_ref().unwrap();
|
||||
result_mutex.lock().unwrap().push((img, Some(r)))
|
||||
result.push((img, Some(r)))
|
||||
}
|
||||
None => result_mutex.lock().unwrap().push((img, None)),
|
||||
None => result.push((img, None)),
|
||||
}
|
||||
});
|
||||
let result = result_mutex.lock().unwrap().clone();
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
pub async fn get_update(image: &str, socket: Option<String>, config: &JsonValue) -> Option<bool> {
|
||||
let local_image = get_image_from_docker_daemon(socket, image).await;
|
||||
let credentials = config["authentication"][&local_image.registry].clone().take_string().or(None);
|
||||
let token = match check_auth(&local_image.registry, config) {
|
||||
Some(auth_url) => get_token(vec![&local_image], &auth_url, &credentials),
|
||||
pub async fn get_update(image: &str, options: &CliConfig) -> Option<bool> {
|
||||
let local_image = get_image_from_docker_daemon(options.socket.clone(), image).await;
|
||||
let credentials = options.config["authentication"][&local_image.registry]
|
||||
.clone()
|
||||
.take_string()
|
||||
.or(None);
|
||||
let client = new_reqwest_client();
|
||||
let token = match check_auth(&local_image.registry, options, &client).await {
|
||||
Some(auth_url) => get_token(vec![&local_image], &auth_url, &credentials, &client).await,
|
||||
None => String::new(),
|
||||
};
|
||||
if options.verbose {
|
||||
debug!("Using token {}", token);
|
||||
};
|
||||
let remote_image = match token.as_str() {
|
||||
"" => get_latest_digest(&local_image, None, config),
|
||||
_ => get_latest_digest(&local_image, Some(&token), config),
|
||||
"" => get_latest_digest(&local_image, None, options, &client).await,
|
||||
_ => get_latest_digest(&local_image, Some(&token), options, &client).await,
|
||||
};
|
||||
match &remote_image.digest {
|
||||
Some(d) => Some(d != &local_image.digest.unwrap()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use bollard::{
|
||||
secret::ImageSummary,
|
||||
ClientVersion, Docker,
|
||||
};
|
||||
use bollard::{secret::ImageSummary, ClientVersion, Docker};
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
use bollard::secret::ImageInspect;
|
||||
use futures::future::join_all;
|
||||
|
||||
use crate::{error, image::Image, utils::split_image};
|
||||
use crate::{
|
||||
error,
|
||||
image::Image,
|
||||
utils::{split_image, CliConfig},
|
||||
};
|
||||
|
||||
fn create_docker_client(socket: Option<String>) -> Docker {
|
||||
let client: Result<Docker, bollard::errors::Error> = match socket {
|
||||
@@ -27,35 +29,24 @@ fn create_docker_client(socket: Option<String>) -> Docker {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_images_from_docker_daemon(socket: Option<String>) -> Vec<Image> {
|
||||
let client: Docker = create_docker_client(socket);
|
||||
pub async fn get_images_from_docker_daemon(options: &CliConfig) -> Vec<Image> {
|
||||
let client: Docker = create_docker_client(options.socket.clone());
|
||||
let images: Vec<ImageSummary> = match client.list_images::<String>(None).await {
|
||||
Ok(images) => images,
|
||||
Err(e) => {
|
||||
error!("Failed to retrieve list of images available!\n{}", e)
|
||||
}
|
||||
};
|
||||
let mut result: Vec<Image> = Vec::new();
|
||||
let mut handles = Vec::new();
|
||||
for image in images {
|
||||
if !image.repo_tags.is_empty() && image.repo_digests.len() == 1 {
|
||||
for t in &image.repo_tags {
|
||||
let (registry, repository, tag) = split_image(t);
|
||||
result.push(Image {
|
||||
registry,
|
||||
repository,
|
||||
tag,
|
||||
digest: Some(
|
||||
image.repo_digests[0]
|
||||
.clone()
|
||||
.split('@')
|
||||
.collect::<Vec<&str>>()[1]
|
||||
.to_string(),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
handles.push(Image::from(image, options))
|
||||
}
|
||||
result
|
||||
join_all(handles)
|
||||
.await
|
||||
.iter()
|
||||
.filter(|img| img.is_some())
|
||||
.map(|img| img.clone().unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
|
||||
@@ -58,7 +58,7 @@ pub fn print_update(name: &str, has_update: &Option<bool>) {
|
||||
}
|
||||
|
||||
pub fn print_raw_update(name: &str, has_update: &Option<bool>) {
|
||||
let result = object! {images: {[name]: *has_update}} ;
|
||||
let result = object! {images: {[name]: *has_update}};
|
||||
println!("{}", result);
|
||||
}
|
||||
|
||||
|
||||
34
src/image.rs
@@ -1,3 +1,10 @@
|
||||
use bollard::secret::ImageSummary;
|
||||
|
||||
use crate::{
|
||||
debug,
|
||||
utils::{split_image, CliConfig},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Image {
|
||||
pub registry: String,
|
||||
@@ -5,3 +12,30 @@ pub struct Image {
|
||||
pub tag: String,
|
||||
pub digest: Option<String>,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub async fn from(image: ImageSummary, options: &CliConfig) -> Option<Self> {
|
||||
if !image.repo_tags.is_empty() && !image.repo_digests.is_empty() {
|
||||
let (registry, repository, tag) = split_image(&image.repo_tags[0]);
|
||||
let image = Image {
|
||||
registry,
|
||||
repository,
|
||||
tag,
|
||||
digest: Some(
|
||||
image.repo_digests[0]
|
||||
.clone()
|
||||
.split('@')
|
||||
.collect::<Vec<&str>>()[1]
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
return Some(image);
|
||||
} else if options.verbose {
|
||||
debug!(
|
||||
"Skipped an image\nTags: {:#?}\nDigests: {:#?}",
|
||||
image.repo_tags, image.repo_digests
|
||||
)
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
52
src/main.rs
@@ -1,22 +1,23 @@
|
||||
#[cfg(feature = "cli")]
|
||||
use check::{get_all_updates, get_update};
|
||||
use chrono::Local;
|
||||
use clap::{Parser, Subcommand};
|
||||
#[cfg(feature = "cli")]
|
||||
use formatting::{print_raw_update, print_raw_updates, print_update, print_updates, Spinner};
|
||||
#[cfg(feature = "cli")]
|
||||
use check::{get_all_updates, get_update};
|
||||
#[cfg(feature = "server")]
|
||||
use server::serve;
|
||||
use std::path::PathBuf;
|
||||
use utils::load_config;
|
||||
use utils::{load_config, CliConfig};
|
||||
|
||||
pub mod check;
|
||||
pub mod docker;
|
||||
pub mod image;
|
||||
pub mod registry;
|
||||
pub mod utils;
|
||||
#[cfg(feature = "cli")]
|
||||
pub mod formatting;
|
||||
pub mod image;
|
||||
pub mod registry;
|
||||
#[cfg(feature = "server")]
|
||||
pub mod server;
|
||||
pub mod utils;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
@@ -25,6 +26,13 @@ struct Cli {
|
||||
socket: Option<String>,
|
||||
#[arg(short, long, default_value_t = String::new(), help = "Config file path")]
|
||||
config_path: String,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value_t = false,
|
||||
help = "Enable verbose (debug) logging"
|
||||
)]
|
||||
verbose: bool,
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
@@ -64,32 +72,52 @@ async fn main() {
|
||||
"" => None,
|
||||
path => Some(PathBuf::from(path)),
|
||||
};
|
||||
let config = load_config(cfg_path);
|
||||
if cli.verbose {
|
||||
debug!("CLI options:");
|
||||
debug!("Config path: {:?}", cfg_path);
|
||||
debug!("Socket: {:?}", &cli.socket)
|
||||
}
|
||||
let cli_config = CliConfig {
|
||||
socket: cli.socket,
|
||||
verbose: cli.verbose,
|
||||
config: load_config(cfg_path),
|
||||
};
|
||||
if cli.verbose {
|
||||
debug!("Config: {}", cli_config.config)
|
||||
}
|
||||
match &cli.command {
|
||||
#[cfg(feature = "cli")]
|
||||
Some(Commands::Check { image, icons, raw }) => match image {
|
||||
Some(name) => {
|
||||
let has_update = get_update(name, cli.socket, &config).await;
|
||||
let has_update = get_update(name, &cli_config).await;
|
||||
match raw {
|
||||
true => print_raw_update(name, &has_update),
|
||||
false => print_update(name, &has_update),
|
||||
};
|
||||
}
|
||||
None => {
|
||||
match raw {
|
||||
true => print_raw_updates(&get_all_updates(cli.socket, &config).await),
|
||||
let start = Local::now().timestamp_millis();
|
||||
match *raw || cli.verbose {
|
||||
true => {
|
||||
let updates = get_all_updates(&cli_config).await;
|
||||
let end = Local::now().timestamp_millis();
|
||||
print_raw_updates(&updates);
|
||||
info!("✨ Checked {} images in {}ms", updates.len(), end - start);
|
||||
}
|
||||
false => {
|
||||
let spinner = Spinner::new();
|
||||
let updates = get_all_updates(cli.socket, &config).await;
|
||||
let updates = get_all_updates(&cli_config).await;
|
||||
spinner.succeed();
|
||||
let end = Local::now().timestamp_millis();
|
||||
print_updates(&updates, icons);
|
||||
info!("✨ Checked {} images in {}ms", updates.len(), end - start);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
#[cfg(feature = "server")]
|
||||
Some(Commands::Serve { port }) => {
|
||||
let _ = serve(port, cli.socket, config).await;
|
||||
let _ = serve(port, &cli_config).await;
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
|
||||
188
src/registry.rs
@@ -1,106 +1,168 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use futures::future::join_all;
|
||||
use json::JsonValue;
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
use ureq::Error;
|
||||
|
||||
use http_auth::parse_challenges;
|
||||
use reqwest_middleware::ClientWithMiddleware;
|
||||
|
||||
use crate::{error, image::Image, warn};
|
||||
use crate::{debug, error, image::Image, utils::CliConfig, warn};
|
||||
|
||||
pub fn check_auth(registry: &str, config: &JsonValue) -> Option<String> {
|
||||
let protocol = if config["insecure_registries"].contains(registry) { "http" } else { "https" };
|
||||
let response = ureq::get(&format!("{}://{}/v2/", protocol, registry)).call();
|
||||
pub async fn check_auth(
|
||||
registry: &str,
|
||||
options: &CliConfig,
|
||||
client: &ClientWithMiddleware,
|
||||
) -> Option<String> {
|
||||
let protocol = if options.config["insecure_registries"].contains(registry) {
|
||||
if options.verbose {
|
||||
debug!(
|
||||
"{} is configured as an insecure registry. Downgrading to HTTP",
|
||||
registry
|
||||
);
|
||||
};
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
};
|
||||
let response = client
|
||||
.get(format!("{}://{}/v2/", protocol, registry))
|
||||
.send()
|
||||
.await;
|
||||
match response {
|
||||
Ok(_) => None,
|
||||
Err(Error::Status(401, response)) => match response.header("www-authenticate") {
|
||||
Some(challenge) => Some(parse_www_authenticate(challenge)),
|
||||
None => error!("Unauthorized to access registry {} and no way to authenticate was provided", registry),
|
||||
},
|
||||
Err(e) => error!("{}", e),
|
||||
Ok(r) => {
|
||||
let status = r.status().as_u16();
|
||||
if status == 401 {
|
||||
match r.headers().get("www-authenticate") {
|
||||
Some(challenge) => Some(parse_www_authenticate(challenge.to_str().unwrap())),
|
||||
None => error!(
|
||||
"Unauthorized to access registry {} and no way to authenticate was provided",
|
||||
registry
|
||||
),
|
||||
}
|
||||
} else if status == 200 {
|
||||
None
|
||||
} else {
|
||||
warn!(
|
||||
"Received unexpected status code {}\nResponse: {}",
|
||||
status,
|
||||
r.text().await.unwrap()
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e.is_connect() {
|
||||
warn!("Connection to registry {} failed.", ®istry);
|
||||
None
|
||||
} else {
|
||||
error!("Unexpected error: {}", e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_latest_digest(image: &Image, token: Option<&String>, config: &JsonValue) -> Image {
|
||||
let protocol = if config["insecure_registries"].contains(json::JsonValue::from(image.registry.clone())) { "http" } else { "https" };
|
||||
let mut request = ureq::head(&format!(
|
||||
pub async fn get_latest_digest(
|
||||
image: &Image,
|
||||
token: Option<&String>,
|
||||
options: &CliConfig,
|
||||
client: &ClientWithMiddleware,
|
||||
) -> Image {
|
||||
let protocol = if options.config["insecure_registries"]
|
||||
.contains(json::JsonValue::from(image.registry.clone()))
|
||||
{
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
};
|
||||
let mut request = client.head(format!(
|
||||
"{}://{}/v2/{}/manifests/{}",
|
||||
protocol, &image.registry, &image.repository, &image.tag
|
||||
));
|
||||
if let Some(t) = token {
|
||||
request = request.set("Authorization", &format!("Bearer {}", t));
|
||||
request = request.header("Authorization", &format!("Bearer {}", t));
|
||||
}
|
||||
let raw_response = match request
|
||||
.set("Accept", "application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json")
|
||||
.call()
|
||||
.header("Accept", "application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json")
|
||||
.send().await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(Error::Status(401, response)) => {
|
||||
if token.is_some() {
|
||||
warn!("Failed to authenticate to registry {} with given token!\n{}", &image.registry, token.unwrap());
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
if status == 401 {
|
||||
if token.is_some() {
|
||||
warn!("Failed to authenticate to registry {} with given token!\n{}", &image.registry, token.unwrap());
|
||||
} else {
|
||||
warn!("Registry requires authentication");
|
||||
}
|
||||
return Image { digest: None, ..image.clone() }
|
||||
} else if status == 404 {
|
||||
warn!("Image {:?} not found", &image);
|
||||
return Image { digest: None, ..image.clone() }
|
||||
} else {
|
||||
return get_latest_digest(
|
||||
image,
|
||||
Some(&get_token(
|
||||
vec![image],
|
||||
&parse_www_authenticate(response.header("www-authenticate").unwrap()),
|
||||
&None // I think?
|
||||
)),
|
||||
config
|
||||
);
|
||||
response
|
||||
}
|
||||
}
|
||||
Err(Error::Status(_, _)) => {
|
||||
return Image {
|
||||
digest: None,
|
||||
..image.clone()
|
||||
},
|
||||
Err(e) => {
|
||||
if e.is_connect() {
|
||||
warn!("Connection to registry failed.");
|
||||
return Image { digest: None, ..image.clone() }
|
||||
} else {
|
||||
error!("Unexpected error: {}", e.to_string())
|
||||
}
|
||||
}
|
||||
Err(ureq::Error::Transport(e)) => error!("Failed to send request!\n{}", e),
|
||||
},
|
||||
};
|
||||
match raw_response.header("docker-content-digest") {
|
||||
match raw_response.headers().get("docker-content-digest") {
|
||||
Some(digest) => Image {
|
||||
digest: Some(digest.to_string()),
|
||||
digest: Some(digest.to_str().unwrap().to_string()),
|
||||
..image.clone()
|
||||
},
|
||||
None => error!("Server returned invalid response! No docker-content-digest!"),
|
||||
None => error!(
|
||||
"Server returned invalid response! No docker-content-digest!\n{:#?}",
|
||||
raw_response
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_latest_digests(images: Vec<&Image>, token: Option<&String>, config: &JsonValue) -> Vec<Image> {
|
||||
let result: Mutex<Vec<Image>> = Mutex::new(Vec::new());
|
||||
images.par_iter().for_each(|&image| {
|
||||
let digest = get_latest_digest(image, token, config).digest;
|
||||
result.lock().unwrap().push(Image {
|
||||
digest,
|
||||
..image.clone()
|
||||
});
|
||||
});
|
||||
let r = result.lock().unwrap().clone();
|
||||
r
|
||||
pub async fn get_latest_digests(
|
||||
images: Vec<&Image>,
|
||||
token: Option<&String>,
|
||||
options: &CliConfig,
|
||||
client: &ClientWithMiddleware,
|
||||
) -> Vec<Image> {
|
||||
let mut handles = Vec::new();
|
||||
for image in images {
|
||||
handles.push(get_latest_digest(image, token, options, client))
|
||||
}
|
||||
join_all(handles).await
|
||||
}
|
||||
|
||||
pub fn get_token(images: Vec<&Image>, auth_url: &str, credentials: &Option<String>) -> String {
|
||||
pub async fn get_token(
|
||||
images: Vec<&Image>,
|
||||
auth_url: &str,
|
||||
credentials: &Option<String>,
|
||||
client: &ClientWithMiddleware,
|
||||
) -> String {
|
||||
let mut final_url = auth_url.to_owned();
|
||||
for image in images {
|
||||
for image in &images {
|
||||
final_url = format!("{}&scope=repository:{}:pull", final_url, image.repository);
|
||||
}
|
||||
let mut base_request = ureq::get(&final_url).set("Accept", "application/vnd.oci.image.index.v1+json"); // Seems to be unnecesarry. Will probably remove in the future
|
||||
let mut base_request = client
|
||||
.get(&final_url)
|
||||
.header("Accept", "application/vnd.oci.image.index.v1+json"); // Seems to be unnecessary. Will probably remove in the future
|
||||
base_request = match credentials {
|
||||
Some(creds) => base_request.set("Authorization", &format!("Basic {}", creds)),
|
||||
None => base_request
|
||||
Some(creds) => base_request.header("Authorization", &format!("Basic {}", creds)),
|
||||
None => base_request,
|
||||
};
|
||||
let raw_response = match base_request.call()
|
||||
{
|
||||
Ok(response) => match response.into_string() {
|
||||
let raw_response = match base_request.send().await {
|
||||
Ok(response) => match response.text().await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
error!("Failed to parse response into string!\n{}", e)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Token request failed!\n{}", e)
|
||||
if e.is_connect() {
|
||||
error!("Connection to registry failed.");
|
||||
} else {
|
||||
error!("Token request failed!\n{}", e.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
let parsed_token_response: JsonValue = match json::parse(&raw_response) {
|
||||
|
||||
106
src/server.rs
@@ -6,7 +6,7 @@ use liquid::{object, Object};
|
||||
use tokio::sync::Mutex;
|
||||
use xitca_web::{
|
||||
body::ResponseBody,
|
||||
handler::{handler_service, state::StateRef},
|
||||
handler::{handler_service, path::PathRef, state::StateRef},
|
||||
http::WebResponse,
|
||||
middleware::Logger,
|
||||
route::get,
|
||||
@@ -15,27 +15,27 @@ use xitca_web::{
|
||||
|
||||
use crate::{
|
||||
check::get_all_updates,
|
||||
error,
|
||||
utils::{sort_update_vec, to_json},
|
||||
error, info,
|
||||
utils::{sort_update_vec, to_json, CliConfig},
|
||||
};
|
||||
|
||||
const RAW_TEMPLATE: &str = include_str!("static/template.liquid");
|
||||
const STYLE: &str = include_str!("static/index.css");
|
||||
const HTML: &str = include_str!("static/index.html");
|
||||
const JS: &str = include_str!("static/assets/index.js");
|
||||
const CSS: &str = include_str!("static/assets/index.css");
|
||||
const FAVICON_ICO: &[u8] = include_bytes!("static/favicon.ico");
|
||||
const FAVICON_SVG: &[u8] = include_bytes!("static/favicon.svg");
|
||||
const APPLE_TOUCH_ICON: &[u8] = include_bytes!("static/apple-touch-icon.png");
|
||||
|
||||
pub async fn serve(port: &u16, socket: Option<String>, config: JsonValue) -> std::io::Result<()> {
|
||||
let mut data = ServerData::new(socket, config).await;
|
||||
data.refresh().await;
|
||||
pub async fn serve(port: &u16, options: &CliConfig) -> std::io::Result<()> {
|
||||
info!("Starting server, please wait...");
|
||||
let data = ServerData::new(options).await;
|
||||
info!("Ready to start!");
|
||||
App::new()
|
||||
.with_state(Arc::new(Mutex::new(data)))
|
||||
.at("/", get(handler_service(home)))
|
||||
.at("/", get(handler_service(_static)))
|
||||
.at("/json", get(handler_service(json)))
|
||||
.at("/refresh", get(handler_service(refresh)))
|
||||
.at("/favicon.ico", handler_service(favicon_ico)) // These aren't pretty but this is xitca-web...
|
||||
.at("/favicon.svg", handler_service(favicon_svg))
|
||||
.at("/apple-touch-icon.png", handler_service(apple_touch_icon))
|
||||
.at("/*", get(handler_service(_static)))
|
||||
.enclosed(Logger::new())
|
||||
.serve()
|
||||
.bind(format!("0.0.0.0:{}", port))?
|
||||
@@ -43,14 +43,46 @@ pub async fn serve(port: &u16, socket: Option<String>, config: JsonValue) -> std
|
||||
.wait()
|
||||
}
|
||||
|
||||
async fn home(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
||||
WebResponse::new(ResponseBody::from(data.lock().await.template.clone()))
|
||||
async fn _static(data: StateRef<'_, Arc<Mutex<ServerData>>>, path: PathRef<'_>) -> WebResponse {
|
||||
match path.0 {
|
||||
"/" => WebResponse::builder()
|
||||
.header("Content-Type", "text/html")
|
||||
.body(ResponseBody::from(data.lock().await.template.clone()))
|
||||
.unwrap(),
|
||||
"/assets/index.js" => WebResponse::builder()
|
||||
.header("Content-Type", "text/javascript")
|
||||
.body(ResponseBody::from(JS.replace(
|
||||
"=\"neutral\"",
|
||||
&format!("=\"{}\"", data.lock().await.theme),
|
||||
)))
|
||||
.unwrap(),
|
||||
"/assets/index.css" => WebResponse::builder()
|
||||
.header("Content-Type", "text/css")
|
||||
.body(ResponseBody::from(CSS))
|
||||
.unwrap(),
|
||||
"/favicon.ico" => WebResponse::builder()
|
||||
.header("Content-Type", "image/vnd.microsoft.icon")
|
||||
.body(ResponseBody::from(FAVICON_ICO))
|
||||
.unwrap(),
|
||||
"/favicon.svg" => WebResponse::builder()
|
||||
.header("Content-Type", "image/svg+xml")
|
||||
.body(ResponseBody::from(FAVICON_SVG))
|
||||
.unwrap(),
|
||||
"/apple-touch-icon.png" => WebResponse::builder()
|
||||
.header("Content-Type", "image/png")
|
||||
.body(ResponseBody::from(APPLE_TOUCH_ICON))
|
||||
.unwrap(),
|
||||
_ => WebResponse::builder()
|
||||
.status(404)
|
||||
.body(ResponseBody::from("Not found"))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn json(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
||||
WebResponse::new(ResponseBody::from(
|
||||
json::stringify(data.lock().await.json.clone())
|
||||
))
|
||||
WebResponse::new(ResponseBody::from(json::stringify(
|
||||
data.lock().await.json.clone(),
|
||||
)))
|
||||
}
|
||||
|
||||
async fn refresh(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
||||
@@ -58,48 +90,37 @@ async fn refresh(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
||||
WebResponse::new(ResponseBody::from("OK"))
|
||||
}
|
||||
|
||||
async fn favicon_ico() -> WebResponse {
|
||||
WebResponse::new(ResponseBody::from(FAVICON_ICO))
|
||||
}
|
||||
|
||||
async fn favicon_svg() -> WebResponse {
|
||||
WebResponse::new(ResponseBody::from(FAVICON_SVG))
|
||||
}
|
||||
|
||||
async fn apple_touch_icon() -> WebResponse {
|
||||
WebResponse::new(ResponseBody::from(APPLE_TOUCH_ICON))
|
||||
}
|
||||
|
||||
struct ServerData {
|
||||
template: String,
|
||||
raw_updates: Vec<(String, Option<bool>)>,
|
||||
json: JsonValue,
|
||||
socket: Option<String>,
|
||||
config: JsonValue,
|
||||
options: CliConfig,
|
||||
theme: &'static str,
|
||||
}
|
||||
|
||||
impl ServerData {
|
||||
async fn new(socket: Option<String>, config: JsonValue) -> Self {
|
||||
async fn new(options: &CliConfig) -> Self {
|
||||
let mut s = Self {
|
||||
socket,
|
||||
options: options.clone(),
|
||||
template: String::new(),
|
||||
json: json::object! {
|
||||
metrics: json::object! {},
|
||||
images: json::object! {},
|
||||
},
|
||||
raw_updates: Vec::new(),
|
||||
config,
|
||||
theme: "neutral",
|
||||
};
|
||||
s.refresh().await;
|
||||
s
|
||||
}
|
||||
async fn refresh(&mut self) {
|
||||
let updates = sort_update_vec(&get_all_updates(self.socket.clone(), &self.config["authentication"]).await);
|
||||
info!("Refreshing data");
|
||||
let updates = sort_update_vec(&get_all_updates(&self.options).await);
|
||||
self.raw_updates = updates;
|
||||
let template = liquid::ParserBuilder::with_stdlib()
|
||||
.build()
|
||||
.unwrap()
|
||||
.parse(RAW_TEMPLATE)
|
||||
.parse(HTML)
|
||||
.unwrap();
|
||||
let images = self
|
||||
.raw_updates
|
||||
@@ -110,8 +131,12 @@ impl ServerData {
|
||||
})
|
||||
.collect::<Vec<Object>>();
|
||||
self.json = to_json(&self.raw_updates);
|
||||
let last_updated = Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||
let theme = match &self.config["theme"].as_str() {
|
||||
let last_updated = Local::now();
|
||||
self.json["last_updated"] = last_updated
|
||||
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
|
||||
.to_string()
|
||||
.into();
|
||||
self.theme = match &self.options.config["theme"].as_str() {
|
||||
Some(t) => match *t {
|
||||
"default" => "neutral",
|
||||
"blue" => "gray",
|
||||
@@ -125,9 +150,8 @@ impl ServerData {
|
||||
let globals = object!({
|
||||
"metrics": [{"name": "Monitored images", "value": self.json["metrics"]["monitored_images"].as_usize()}, {"name": "Up to date", "value": self.json["metrics"]["up_to_date"].as_usize()}, {"name": "Updates available", "value": self.json["metrics"]["update_available"].as_usize()}, {"name": "Unknown", "value": self.json["metrics"]["unknown"].as_usize()}],
|
||||
"images": images,
|
||||
"style": STYLE,
|
||||
"last_updated": last_updated.to_string(),
|
||||
"theme": theme
|
||||
"last_updated": last_updated.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
"theme": &self.theme
|
||||
});
|
||||
self.template = template.render(&globals).unwrap();
|
||||
}
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Cup</title>
|
||||
<style>
|
||||
{{ style }}
|
||||
</style>
|
||||
<style>
|
||||
/* Kinda hacky, but thank you https://geary.co/internal-borders-css-grid/ */
|
||||
.gi {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gi::before,
|
||||
.gi::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
{% if theme == "neutral" %}
|
||||
background-color: #e5e5e5;
|
||||
{% elsif theme == "gray" %}
|
||||
background-color: #e5e7eb
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.gi::before,
|
||||
.gi::after {
|
||||
{% if theme == "neutral" %}
|
||||
background-color: #262626;
|
||||
{% elsif theme == "gray" %}
|
||||
background-color: #1f2937
|
||||
{% endif %}
|
||||
}
|
||||
}
|
||||
|
||||
.gi::before {
|
||||
inline-size: 1px;
|
||||
block-size: 100vh;
|
||||
inset-inline-start: -0.125rem;
|
||||
}
|
||||
|
||||
.gi::after {
|
||||
inline-size: 100vw;
|
||||
block-size: 1px;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: -0.12rem;
|
||||
}
|
||||
|
||||
@supports (scrollbar-color: auto) {
|
||||
html {
|
||||
scrollbar-color: #707070 #343840;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
html::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-track {
|
||||
background: #343840;
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-thumb {
|
||||
background: #707070;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-thumb:hover {
|
||||
background: #b5b5b5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function refresh(event) {
|
||||
var button = event.currentTarget;
|
||||
button.disabled = true;
|
||||
|
||||
let request = new XMLHttpRequest();
|
||||
request.onload = function () {
|
||||
if (request.status === 200) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
request.open('GET', `${window.location.origin}/refresh`);
|
||||
request.send();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex justify-center items-center min-h-screen bg-{{ theme }}-50 dark:bg-{{ theme }}-950">
|
||||
<div class="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full">
|
||||
<div class="max-w-[48rem] mx-auto h-full my-8">
|
||||
<div class="flex items-center gap-1">
|
||||
<h1 class="text-5xl lg:text-6xl font-bold dark:text-white">Cup</h1>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 128 128"
|
||||
style="enable-background:new 0 0 128 128;"
|
||||
xml:space="preserve"
|
||||
class="size-16"
|
||||
>
|
||||
<path style="fill:#A6CFD6;" d="M65.12,17.55c-17.6-0.53-34.75,5.6-34.83,14.36c-0.04,5.2,1.37,18.6,3.62,48.68s2.25,33.58,3.5,34.95
|
||||
c1.25,1.37,10.02,8.8,25.75,8.8s25.93-6.43,26.93-8.05c0.48-0.78,1.83-17.89,3.5-37.07c1.81-20.84,3.91-43.9,3.99-45.06
|
||||
C97.82,30.66,94.2,18.43,65.12,17.55z"/>
|
||||
<path style="fill:#DCEDF6;" d="M41.4,45.29c-0.12,0.62,1.23,24.16,2.32,27.94c1.99,6.92,9.29,7.38,10.23,4.16
|
||||
c0.9-3.07-0.38-29.29-0.38-29.29s-3.66-0.3-6.43-0.84C44,46.63,41.4,45.29,41.4,45.29z"/>
|
||||
<path style="fill:#6CA4AE;" d="M33.74,32.61c-0.26,8.83,20.02,12.28,30.19,12.22c13.56-0.09,29.48-4.29,29.8-11.7
|
||||
S79.53,21.1,63.35,21.1C49.6,21.1,33.96,25.19,33.74,32.61z"/>
|
||||
<path style="fill:#DC0D27;" d="M84.85,13.1c-0.58,0.64-9.67,30.75-9.67,30.75s2.01-0.33,4-0.79c2.63-0.61,3.76-1.06,3.76-1.06
|
||||
s7.19-22.19,7.64-23.09c0.45-0.9,21.61-7.61,22.31-7.93c0.7-0.32,1.39-0.4,1.46-0.78c0.06-0.38-2.34-6.73-3.11-6.73
|
||||
C110.47,3.47,86.08,11.74,84.85,13.1z"/>
|
||||
<path style="fill:#8A1F0F;" d="M110.55,7.79c1.04,2.73,2.8,3.09,3.55,2.77c0.45-0.19,1.25-1.84,0.01-4.47
|
||||
c-0.99-2.09-2.17-2.74-2.93-2.61C110.42,3.6,109.69,5.53,110.55,7.79z"/>
|
||||
<g>
|
||||
<path style="fill:#8A1F0F;" d="M91.94,18.34c-0.22,0-0.44-0.11-0.58-0.3l-3.99-5.77c-0.22-0.32-0.14-0.75,0.18-0.97
|
||||
c0.32-0.22,0.76-0.14,0.97,0.18l3.99,5.77c0.22,0.32,0.14,0.75-0.18,0.97C92.21,18.3,92.07,18.34,91.94,18.34z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#8A1F0F;" d="M90.28,19.43c-0.18,0-0.35-0.07-0.49-0.2l-5.26-5.12c-0.28-0.27-0.28-0.71-0.01-0.99
|
||||
c0.27-0.28,0.71-0.28,0.99-0.01l5.26,5.12c0.28,0.27,0.28,0.71,0.01,0.99C90.64,19.36,90.46,19.43,90.28,19.43z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#8A1F0F;" d="M89.35,21.22c-0.12,0-0.25-0.03-0.36-0.1l-5.6-3.39c-0.33-0.2-0.44-0.63-0.24-0.96
|
||||
c0.2-0.33,0.63-0.44,0.96-0.24l5.6,3.39c0.33,0.2,0.44,0.63,0.24,0.96C89.82,21.1,89.59,21.22,89.35,21.22z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="shadow-sm bg-white dark:bg-{{ theme }}-900 rounded-md my-8">
|
||||
<dl class="lg:grid-cols-4 grid-cols-2 gap-1 grid overflow-hidden *:relative">
|
||||
{% for metric in metrics %}
|
||||
<div class="gi">
|
||||
<div class="xl:px-8 px-6 py-4 gap-y-2 gap-x-4 justify-between align-baseline flex flex-col h-full">
|
||||
<dt class="text-{{ theme }}-500 dark:text-{{ theme }}-400 leading-6 font-medium flex gap-1 justify-between">
|
||||
{{ metric.name }}
|
||||
{% if metric.name == 'Monitored images' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-black dark:text-white shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 4c4.29 0 7.863 2.429 10.665 7.154l.22 .379l.045 .1l.03 .083l.014 .055l.014 .082l.011 .1v.11l-.014 .111a.992 .992 0 0 1 -.026 .11l-.039 .108l-.036 .075l-.016 .03c-2.764 4.836 -6.3 7.38 -10.555 7.499l-.313 .004c-4.396 0 -8.037 -2.549 -10.868 -7.504a1 1 0 0 1 0 -.992c2.831 -4.955 6.472 -7.504 10.868 -7.504zm0 5a3 3 0 1 0 0 6a3 3 0 0 0 0 -6z" />
|
||||
</svg>
|
||||
{% elsif metric.name == 'Up to date' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-green-500 shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" />
|
||||
</svg>
|
||||
{% elsif metric.name == 'Updates available' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-blue-500 shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z" />
|
||||
</svg>
|
||||
{% elsif metric.name == 'Unknown' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-{{ theme }}-500 shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z" />
|
||||
</svg>
|
||||
{% endif %}
|
||||
</dt>
|
||||
<dd class="text-black dark:text-white tracking-tight leading-10 font-medium text-3xl flex-none w-full">
|
||||
{{ metric.value }}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
</div>
|
||||
<div class="shadow-sm bg-white dark:bg-{{ theme }}-900 rounded-md my-8">
|
||||
<div class="flex justify-between items-center px-6 py-4 text-{{ theme }}-500">
|
||||
<h3>Last checked: {{ last_updated }}</h3>
|
||||
<button class="group" onclick="refresh(event)">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="group-disabled:animate-spin"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4,11A8.1,8.1 0 0 1 19.5,9M20,5v4h-4" /><path d="M20,13A8.1,8.1 0 0 1 4.5,15M4,19v-4h4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-{{ theme }}-800 divide-y dark:text-white">
|
||||
{% for image in images %}
|
||||
<li>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M21 16.008v-8.018a1.98 1.98 0 0 0 -1 -1.717l-7 -4.008a2.016 2.016 0 0 0 -2 0l-7 4.008c-.619 .355 -1 1.01 -1 1.718v8.018c0 .709 .381 1.363 1 1.717l7 4.008a2.016 2.016 0 0 0 2 0l7 -4.008c.619 -.355 1 -1.01 1 -1.718z" />
|
||||
<path d="M12 22v-10" />
|
||||
<path d="M12 12l8.73 -5.04" />
|
||||
<path d="M3.27 6.96l8.73 5.04" />
|
||||
</svg>
|
||||
{{ image.name }}
|
||||
{% if image.has_update == 'false' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="text-green-500 ml-auto"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" />
|
||||
</svg>
|
||||
{% elsif image.has_update == 'true' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="text-blue-500 ml-auto"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z" />
|
||||
</svg>
|
||||
{% elsif image.has_update == 'null' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="text-{{ theme }}-500 ml-auto"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z" />
|
||||
</svg>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
114
src/utils.rs
@@ -1,40 +1,27 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{error, image::Image};
|
||||
use json::{object, JsonValue};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
||||
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
|
||||
/// This macro is an alternative to panic. It prints the message you give it and exits the process with code 1, without printing a stack trace. Useful for when the program has to exit due to a user error or something unexpected which is unrelated to the program (e.g. a failed web request)
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($($arg:tt)*) => ({
|
||||
eprintln!($($arg)*);
|
||||
std::process::exit(1);
|
||||
})
|
||||
}
|
||||
|
||||
// A small macro to print in yellow as a warning
|
||||
#[macro_export]
|
||||
macro_rules! warn {
|
||||
($($arg:tt)*) => ({
|
||||
eprintln!("\x1b[93m{}\x1b[0m", format!($($arg)*));
|
||||
})
|
||||
}
|
||||
static RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r#"^(?P<name>(?:(?P<registry>(?:(?:localhost|[\w-]+(?:\.[\w-]+)+)(?::\d+)?)|[\w]+:\d+)/)?(?P<repository>[a-z0-9_.-]+(?:/[a-z0-9_.-]+)*))(?::(?P<tag>[\w][\w.-]{0,127}))?$"#, // From https://regex101.com/r/nmSDPA/1
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
/// Takes an image and splits it into registry, repository and tag. For example ghcr.io/sergi0g/cup:latest becomes ['ghcr.io', 'sergi0g/cup', 'latest'].
|
||||
pub fn split_image(image: &str) -> (String, String, String) {
|
||||
static RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r#"^(?P<name>(?:(?P<registry>(?:(?:localhost|[\w-]+(?:\.[\w-]+)+)(?::\d+)?)|[\w]+:\d+)/)?(?P<repository>[a-z0-9_.-]+(?:/[a-z0-9_.-]+)*))(?::(?P<tag>[\w][\w.-]{0,127}))?$"#, // From https://regex101.com/r/nmSDPA/1
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
match RE.captures(image) {
|
||||
Some(c) => {
|
||||
let registry = match c.name("registry") {
|
||||
Some(registry) => registry.as_str().to_owned(),
|
||||
None => String::from("registry-1.docker.io"),
|
||||
};
|
||||
Some(registry) => registry.as_str().to_owned(),
|
||||
None => String::from("registry-1.docker.io"),
|
||||
};
|
||||
return (
|
||||
registry.clone(),
|
||||
match c.name("repository") {
|
||||
@@ -52,29 +39,29 @@ pub fn split_image(image: &str) -> (String, String, String) {
|
||||
Some(tag) => tag.as_str().to_owned(),
|
||||
None => String::from("latest"),
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
None => error!("Failed to parse image {}", image),
|
||||
}
|
||||
}
|
||||
|
||||
/// Given an image's parts which were previously created by split_image, recreate a reference that docker would use. This means removing the registry part, if it's Docker Hub and removing "library" if the image is official
|
||||
pub fn unsplit_image(registry: &str, repository: &str, tag: &str) -> String {
|
||||
let reg = match registry {
|
||||
pub fn unsplit_image(image: &Image) -> String {
|
||||
let reg = match image.registry.as_str() {
|
||||
"registry-1.docker.io" => String::new(),
|
||||
r => format!("{}/", r),
|
||||
};
|
||||
let repo = match repository.split('/').collect::<Vec<&str>>()[0] {
|
||||
let repo = match image.repository.split('/').collect::<Vec<&str>>()[0] {
|
||||
"library" => {
|
||||
if reg.is_empty() {
|
||||
repository.strip_prefix("library/").unwrap()
|
||||
image.repository.strip_prefix("library/").unwrap()
|
||||
} else {
|
||||
repository
|
||||
image.repository.as_str()
|
||||
}
|
||||
}
|
||||
_ => repository,
|
||||
_ => image.repository.as_str(),
|
||||
};
|
||||
format!("{}{}:{}", reg, repo, tag)
|
||||
format!("{}{}:{}", reg, repo, image.tag)
|
||||
}
|
||||
|
||||
/// Sorts the update vector alphabetically and where Some(true) > Some(false) > None
|
||||
@@ -124,21 +111,64 @@ pub fn to_json(updates: &[(String, Option<bool>)]) -> JsonValue {
|
||||
let up_to_date = updates
|
||||
.iter()
|
||||
.filter(|&(_, value)| *value == Some(false))
|
||||
.collect::<Vec<&(String, Option<bool>)>>()
|
||||
.len();
|
||||
.count();
|
||||
let update_available = updates
|
||||
.iter()
|
||||
.filter(|&(_, value)| *value == Some(true))
|
||||
.collect::<Vec<&(String, Option<bool>)>>()
|
||||
.len();
|
||||
let unknown = updates
|
||||
.iter()
|
||||
.filter(|&(_, value)| value.is_none())
|
||||
.collect::<Vec<&(String, Option<bool>)>>()
|
||||
.len();
|
||||
.count();
|
||||
let unknown = updates.iter().filter(|&(_, value)| value.is_none()).count();
|
||||
let _ = json_data["metrics"].insert("monitored_images", updates.len());
|
||||
let _ = json_data["metrics"].insert("up_to_date", up_to_date);
|
||||
let _ = json_data["metrics"].insert("update_available", update_available);
|
||||
let _ = json_data["metrics"].insert("unknown", unknown);
|
||||
json_data
|
||||
}
|
||||
|
||||
/// Struct to hold some config values to avoid having to pass them all the time
|
||||
#[derive(Clone)]
|
||||
pub struct CliConfig {
|
||||
pub socket: Option<String>,
|
||||
pub verbose: bool,
|
||||
pub config: JsonValue,
|
||||
}
|
||||
|
||||
// Logging
|
||||
|
||||
/// This macro is an alternative to panic. It prints the message you give it and exits the process with code 1, without printing a stack trace. Useful for when the program has to exit due to a user error or something unexpected which is unrelated to the program (e.g. a failed web request)
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($($arg:tt)*) => ({
|
||||
eprintln!("\x1b[41m ERROR \x1b[0m {}", format!($($arg)*));
|
||||
std::process::exit(1);
|
||||
})
|
||||
}
|
||||
|
||||
// A small macro to print in yellow as a warning
|
||||
#[macro_export]
|
||||
macro_rules! warn {
|
||||
($($arg:tt)*) => ({
|
||||
eprintln!("\x1b[103m WARN \x1b[0m {}", format!($($arg)*));
|
||||
})
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
($($arg:tt)*) => ({
|
||||
println!("\x1b[44m INFO \x1b[0m {}", format!($($arg)*));
|
||||
})
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! debug {
|
||||
($($arg:tt)*) => ({
|
||||
println!("\x1b[48:5:57m DEBUG \x1b[0m {}", format!($($arg)*));
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_reqwest_client() -> ClientWithMiddleware {
|
||||
ClientBuilder::new(reqwest::Client::new())
|
||||
.with(RetryTransientMiddleware::new_with_policy(
|
||||
ExponentialBackoff::builder().build_with_max_retries(3),
|
||||
))
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"src/static/template.liquid"
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
safelist: [
|
||||
{
|
||||
pattern: /(bg|text|divide)-(gray|neutral)-.+/,
|
||||
variants: ["dark"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
24
web/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
1
web/.tool-versions
Normal file
@@ -0,0 +1 @@
|
||||
nodejs 22.8.0
|
||||
9
web/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Cup web frontend
|
||||
|
||||
This is the Cup web frontend, built with Vite and React. Once it's built, Cup modifies a few things (notably the theme) and sends the result to the client.
|
||||
|
||||
# Development
|
||||
|
||||
Requirements: Bun, Node.js 20+
|
||||
|
||||
Install dependencies with `bun install` and start the development server with `bun dev`.
|
||||
BIN
web/bun.lockb
Executable file
36
web/eslint.config.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
273
web/index.html
Normal file
@@ -0,0 +1,273 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>Cup</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<div
|
||||
class="flex justify-center items-center min-h-screen bg-{{ theme }}-50 dark:bg-{{ theme }}-950"
|
||||
>
|
||||
<div class="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full">
|
||||
<div class="max-w-[48rem] mx-auto h-full my-8">
|
||||
<div class="flex items-center gap-1">
|
||||
<h1 class="text-5xl lg:text-6xl font-bold dark:text-white">
|
||||
Cup
|
||||
</h1>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 128 128"
|
||||
style="enable-background: new 0 0 128 128"
|
||||
xml:space="preserve"
|
||||
class="size-14 lg:size-16"
|
||||
>
|
||||
<path
|
||||
style="fill: #a6cfd6"
|
||||
d="M65.12,17.55c-17.6-0.53-34.75,5.6-34.83,14.36c-0.04,5.2,1.37,18.6,3.62,48.68s2.25,33.58,3.5,34.95
|
||||
c1.25,1.37,10.02,8.8,25.75,8.8s25.93-6.43,26.93-8.05c0.48-0.78,1.83-17.89,3.5-37.07c1.81-20.84,3.91-43.9,3.99-45.06
|
||||
C97.82,30.66,94.2,18.43,65.12,17.55z"
|
||||
/>
|
||||
<path
|
||||
style="fill: #dcedf6"
|
||||
d="M41.4,45.29c-0.12,0.62,1.23,24.16,2.32,27.94c1.99,6.92,9.29,7.38,10.23,4.16
|
||||
c0.9-3.07-0.38-29.29-0.38-29.29s-3.66-0.3-6.43-0.84C44,46.63,41.4,45.29,41.4,45.29z"
|
||||
/>
|
||||
<path
|
||||
style="fill: #6ca4ae"
|
||||
d="M33.74,32.61c-0.26,8.83,20.02,12.28,30.19,12.22c13.56-0.09,29.48-4.29,29.8-11.7
|
||||
S79.53,21.1,63.35,21.1C49.6,21.1,33.96,25.19,33.74,32.61z"
|
||||
/>
|
||||
<path
|
||||
style="fill: #dc0d27"
|
||||
d="M84.85,13.1c-0.58,0.64-9.67,30.75-9.67,30.75s2.01-0.33,4-0.79c2.63-0.61,3.76-1.06,3.76-1.06
|
||||
s7.19-22.19,7.64-23.09c0.45-0.9,21.61-7.61,22.31-7.93c0.7-0.32,1.39-0.4,1.46-0.78c0.06-0.38-2.34-6.73-3.11-6.73
|
||||
C110.47,3.47,86.08,11.74,84.85,13.1z"
|
||||
/>
|
||||
<path
|
||||
style="fill: #8a1f0f"
|
||||
d="M110.55,7.79c1.04,2.73,2.8,3.09,3.55,2.77c0.45-0.19,1.25-1.84,0.01-4.47
|
||||
c-0.99-2.09-2.17-2.74-2.93-2.61C110.42,3.6,109.69,5.53,110.55,7.79z"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
style="fill: #8a1f0f"
|
||||
d="M91.94,18.34c-0.22,0-0.44-0.11-0.58-0.3l-3.99-5.77c-0.22-0.32-0.14-0.75,0.18-0.97
|
||||
c0.32-0.22,0.76-0.14,0.97,0.18l3.99,5.77c0.22,0.32,0.14,0.75-0.18,0.97C92.21,18.3,92.07,18.34,91.94,18.34z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
style="fill: #8a1f0f"
|
||||
d="M90.28,19.43c-0.18,0-0.35-0.07-0.49-0.2l-5.26-5.12c-0.28-0.27-0.28-0.71-0.01-0.99
|
||||
c0.27-0.28,0.71-0.28,0.99-0.01l5.26,5.12c0.28,0.27,0.28,0.71,0.01,0.99C90.64,19.36,90.46,19.43,90.28,19.43z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
style="fill: #8a1f0f"
|
||||
d="M89.35,21.22c-0.12,0-0.25-0.03-0.36-0.1l-5.6-3.39c-0.33-0.2-0.44-0.63-0.24-0.96
|
||||
c0.2-0.33,0.63-0.44,0.96-0.24l5.6,3.39c0.33,0.2,0.44,0.63,0.24,0.96C89.82,21.1,89.59,21.22,89.35,21.22z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="shadow-sm bg-white dark:bg-{{ theme }}-900 rounded-md my-8"
|
||||
>
|
||||
<dl
|
||||
class="lg:grid-cols-4 grid-cols-2 gap-1 grid overflow-hidden *:relative"
|
||||
>
|
||||
{% for metric in metrics %}
|
||||
<div class="gi">
|
||||
<div
|
||||
class="xl:px-8 px-6 py-4 gap-y-2 gap-x-4 justify-between align-baseline flex flex-col h-full"
|
||||
>
|
||||
<dt
|
||||
class="text-{{ theme }}-500 dark:text-{{ theme }}-400 leading-6 font-medium"
|
||||
>
|
||||
{{ metric.name }}
|
||||
</dt>
|
||||
<div class="flex gap-1 justify-between items-center">
|
||||
<dd
|
||||
class="text-black dark:text-white tracking-tight leading-10 font-medium text-3xl w-full"
|
||||
>
|
||||
{{ metric.value }}
|
||||
</dd>
|
||||
{% if metric.name == 'Monitored images' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-black dark:text-white shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 4c4.29 0 7.863 2.429 10.665 7.154l.22 .379l.045 .1l.03 .083l.014 .055l.014 .082l.011 .1v.11l-.014 .111a.992 .992 0 0 1 -.026 .11l-.039 .108l-.036 .075l-.016 .03c-2.764 4.836 -6.3 7.38 -10.555 7.499l-.313 .004c-4.396 0 -8.037 -2.549 -10.868 -7.504a1 1 0 0 1 0 -.992c2.831 -4.955 6.472 -7.504 10.868 -7.504zm0 5a3 3 0 1 0 0 6a3 3 0 0 0 0 -6z"
|
||||
/>
|
||||
</svg>
|
||||
{% elsif metric.name == 'Up to date' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-green-500 shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z"
|
||||
/>
|
||||
</svg>
|
||||
{% elsif metric.name == 'Updates available' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-blue-500 shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z"
|
||||
/>
|
||||
</svg>
|
||||
{% elsif metric.name == 'Unknown' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-{{ theme }}-500 shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z"
|
||||
/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
</div>
|
||||
<div
|
||||
class="shadow-sm bg-white dark:bg-{{ theme }}-900 rounded-md my-8"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center px-6 py-4 text-{{ theme }}-500"
|
||||
>
|
||||
<h3>Last checked: {{ last_updated }}</h3>
|
||||
<button class="group" onclick="refresh(event)">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="group-disabled:animate-spin"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4,11A8.1,8.1 0 0 1 19.5,9M20,5v4h-4" />
|
||||
<path d="M20,13A8.1,8.1 0 0 1 4.5,15M4,19v-4h4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<ul
|
||||
class="*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-{{ theme }}-800 divide-y dark:text-white"
|
||||
>
|
||||
{% for image in images %}
|
||||
<li>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M21 16.008v-8.018a1.98 1.98 0 0 0 -1 -1.717l-7 -4.008a2.016 2.016 0 0 0 -2 0l-7 4.008c-.619 .355 -1 1.01 -1 1.718v8.018c0 .709 .381 1.363 1 1.717l7 4.008a2.016 2.016 0 0 0 2 0l7 -4.008c.619 -.355 1 -1.01 1 -1.718z"
|
||||
/>
|
||||
<path d="M12 22v-10" />
|
||||
<path d="M12 12l8.73 -5.04" />
|
||||
<path d="M3.27 6.96l8.73 5.04" />
|
||||
</svg>
|
||||
{{ image.name }} {% if image.has_update == 'false' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="text-green-500 ml-auto"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z"
|
||||
/>
|
||||
</svg>
|
||||
{% elsif image.has_update == 'true' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="text-blue-500 ml-auto"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z"
|
||||
/>
|
||||
</svg>
|
||||
{% elsif image.has_update == 'null' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="text-{{ theme }}-500 ml-auto"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z"
|
||||
/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
41
web/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"fmt": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@tabler/icons-react": "^3.14.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@types/node": "^22.5.1",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"globals": "^15.9.0",
|
||||
"postcss": "^8.4.42",
|
||||
"prettier": "^3.3.3",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.0.1",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
}
|
||||
6
web/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
61
web/src/App.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useState } from "react";
|
||||
import Logo from "./components/Logo";
|
||||
import Statistic from "./components/Statistic";
|
||||
import Image from "./components/Image";
|
||||
import { LastChecked } from "./components/LastChecked";
|
||||
import Loading from "./components/Loading";
|
||||
import { Data } from "./types";
|
||||
import { theme } from "./theme";
|
||||
import RefreshButton from "./components/RefreshButton";
|
||||
import Search from "./components/Search";
|
||||
|
||||
function App() {
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
if (!data) return <Loading onLoad={setData} />;
|
||||
return (
|
||||
<div
|
||||
className={`flex justify-center min-h-screen bg-${theme}-50 dark:bg-${theme}-950`}
|
||||
>
|
||||
<div className="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full">
|
||||
<div className="flex flex-col max-w-[48rem] mx-auto h-full my-8">
|
||||
<div className="flex items-center gap-1">
|
||||
<h1 className="text-5xl lg:text-6xl font-bold dark:text-white">
|
||||
Cup
|
||||
</h1>
|
||||
<Logo />
|
||||
</div>
|
||||
<div
|
||||
className={`shadow-sm bg-white dark:bg-${theme}-900 rounded-md my-8`}
|
||||
>
|
||||
<dl className="lg:grid-cols-4 md:grid-cols-2 gap-1 grid-cols-1 grid overflow-hidden *:relative">
|
||||
{Object.entries(data.metrics).map(([name, value]) => (
|
||||
<Statistic name={name} value={value} key={name} />
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
<div
|
||||
className={`shadow-sm bg-white dark:bg-${theme}-900 rounded-md my-8`}
|
||||
>
|
||||
<div
|
||||
className={`flex justify-between items-center px-6 py-4 text-${theme}-500`}
|
||||
>
|
||||
<LastChecked datetime={data.last_updated} />
|
||||
<RefreshButton />
|
||||
</div>
|
||||
<Search onChange={setSearchQuery}/>
|
||||
<ul
|
||||
className={`*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-${theme}-800 divide-y dark:text-white`}
|
||||
>
|
||||
{Object.entries(data.images).filter(([name]) => name.includes(searchQuery)).map(([name, status]) => (
|
||||
<Image name={name} status={status} key={name} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
46
web/src/components/Image.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
IconCircleArrowUpFilled,
|
||||
IconCircleCheckFilled,
|
||||
IconCube,
|
||||
IconHelpCircleFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import { WithTooltip } from "./Tooltip";
|
||||
|
||||
export default function Image({
|
||||
name,
|
||||
status,
|
||||
}: {
|
||||
name: string;
|
||||
status: boolean | null;
|
||||
}) {
|
||||
return (
|
||||
<li className="break-all">
|
||||
<IconCube className="size-6 shrink-0" />
|
||||
{name}
|
||||
{status == false && (
|
||||
<WithTooltip
|
||||
text="Up to date"
|
||||
className="text-green-500 ml-auto size-6 shrink-0"
|
||||
>
|
||||
<IconCircleCheckFilled />
|
||||
</WithTooltip>
|
||||
)}
|
||||
{status == true && (
|
||||
<WithTooltip
|
||||
text="Update available"
|
||||
className="text-blue-500 ml-auto size-6 shrink-0"
|
||||
>
|
||||
<IconCircleArrowUpFilled />
|
||||
</WithTooltip>
|
||||
)}
|
||||
{status == null && (
|
||||
<WithTooltip
|
||||
text="Unknown"
|
||||
className="text-gray-500 ml-auto size-6 shrink-0"
|
||||
>
|
||||
<IconHelpCircleFilled />
|
||||
</WithTooltip>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
6
web/src/components/LastChecked.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { intlFormatDistance } from "date-fns/intlFormatDistance";
|
||||
|
||||
export function LastChecked({ datetime }: { datetime: string }) {
|
||||
const date = intlFormatDistance(new Date(datetime), new Date());
|
||||
return <h3>Last checked {date}</h3>;
|
||||
}
|
||||
34
web/src/components/Loading.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { IconLoader2 } from "@tabler/icons-react";
|
||||
import { Data } from "../types";
|
||||
import Logo from "./Logo";
|
||||
import { theme } from "../theme";
|
||||
|
||||
export default function Loading({ onLoad }: { onLoad: (data: Data) => void }) {
|
||||
fetch(
|
||||
process.env.NODE_ENV === "production"
|
||||
? "/json"
|
||||
: `http://${window.location.hostname}:8000/json`,
|
||||
).then((response) => response.json().then((data) => {onLoad(data as Data)}));
|
||||
return (
|
||||
<div
|
||||
className={`flex justify-center min-h-screen bg-${theme}-50 dark:bg-${theme}-950`}
|
||||
>
|
||||
<div className="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full absolute overflow-hidden">
|
||||
<div className="flex flex-col max-w-[48rem] mx-auto h-full my-8">
|
||||
<div className="flex items-center gap-1">
|
||||
<h1 className="text-5xl lg:text-6xl font-bold dark:text-white">
|
||||
Cup
|
||||
</h1>
|
||||
<Logo />
|
||||
</div>
|
||||
<div
|
||||
className={`h-full flex justify-center
|
||||
items-center gap-1 text-${theme}-500 dark:text-${theme}-400`}
|
||||
>
|
||||
Loading <IconLoader2 className="animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
web/src/components/Logo.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
export default function Logo() {
|
||||
return (
|
||||
<svg viewBox="0 0 128 128" className="size-14 lg:size-16">
|
||||
<path
|
||||
style={{ fill: "#A6CFD6" }}
|
||||
d="M65.12,17.55c-17.6-0.53-34.75,5.6-34.83,14.36c-0.04,5.2,1.37,18.6,3.62,48.68s2.25,33.58,3.5,34.95
|
||||
c1.25,1.37,10.02,8.8,25.75,8.8s25.93-6.43,26.93-8.05c0.48-0.78,1.83-17.89,3.5-37.07c1.81-20.84,3.91-43.9,3.99-45.06
|
||||
C97.82,30.66,94.2,18.43,65.12,17.55z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#DCEDF6" }}
|
||||
d="M41.4,45.29c-0.12,0.62,1.23,24.16,2.32,27.94c1.99,6.92,9.29,7.38,10.23,4.16
|
||||
c0.9-3.07-0.38-29.29-0.38-29.29s-3.66-0.3-6.43-0.84C44,46.63,41.4,45.29,41.4,45.29z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#6CA4AE" }}
|
||||
d="M33.74,32.61c-0.26,8.83,20.02,12.28,30.19,12.22c13.56-0.09,29.48-4.29,29.8-11.7
|
||||
S79.53,21.1,63.35,21.1C49.6,21.1,33.96,25.19,33.74,32.61z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#DC0D27" }}
|
||||
d="M84.85,13.1c-0.58,0.64-9.67,30.75-9.67,30.75s2.01-0.33,4-0.79c2.63-0.61,3.76-1.06,3.76-1.06
|
||||
s7.19-22.19,7.64-23.09c0.45-0.9,21.61-7.61,22.31-7.93c0.7-0.32,1.39-0.4,1.46-0.78c0.06-0.38-2.34-6.73-3.11-6.73
|
||||
C110.47,3.47,86.08,11.74,84.85,13.1z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#8A1F0F" }}
|
||||
d="M110.55,7.79c1.04,2.73,2.8,3.09,3.55,2.77c0.45-0.19,1.25-1.84,0.01-4.47
|
||||
c-0.99-2.09-2.17-2.74-2.93-2.61C110.42,3.6,109.69,5.53,110.55,7.79z"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: "#8A1F0F" }}
|
||||
d="M91.94,18.34c-0.22,0-0.44-0.11-0.58-0.3l-3.99-5.77c-0.22-0.32-0.14-0.75,0.18-0.97
|
||||
c0.32-0.22,0.76-0.14,0.97,0.18l3.99,5.77c0.22,0.32,0.14,0.75-0.18,0.97C92.21,18.3,92.07,18.34,91.94,18.34z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: "#8A1F0F" }}
|
||||
d="M90.28,19.43c-0.18,0-0.35-0.07-0.49-0.2l-5.26-5.12c-0.28-0.27-0.28-0.71-0.01-0.99
|
||||
c0.27-0.28,0.71-0.28,0.99-0.01l5.26,5.12c0.28,0.27,0.28,0.71,0.01,0.99C90.64,19.36,90.46,19.43,90.28,19.43z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: "#8A1F0F" }}
|
||||
d="M89.35,21.22c-0.12,0-0.25-0.03-0.36-0.1l-5.6-3.39c-0.33-0.2-0.44-0.63-0.24-0.96
|
||||
c0.2-0.33,0.63-0.44,0.96-0.24l5.6,3.39c0.33,0.2,0.44,0.63,0.24,0.96C89.82,21.1,89.59,21.22,89.35,21.22z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
45
web/src/components/RefreshButton.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { MouseEvent } from "react";
|
||||
import { WithTooltip } from "./Tooltip";
|
||||
|
||||
export default function RefreshButton() {
|
||||
const refresh = (event: MouseEvent) => {
|
||||
const btn = event.currentTarget as HTMLButtonElement;
|
||||
btn.disabled = true;
|
||||
|
||||
const request = new XMLHttpRequest();
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
request.open(
|
||||
"GET",
|
||||
process.env.NODE_ENV === "production"
|
||||
? "/refresh"
|
||||
: `http://${window.location.hostname}:8000/refresh`,
|
||||
);
|
||||
request.send();
|
||||
};
|
||||
return (
|
||||
<WithTooltip text="Reload">
|
||||
<button className="group" onClick={refresh}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="group-disabled:animate-spin"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4,11A8.1,8.1 0 0 1 19.5,9M20,5v4h-4" />
|
||||
<path d="M20,13A8.1,8.1 0 0 1 4.5,15M4,19v-4h4" />
|
||||
</svg>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
);
|
||||
}
|
||||
51
web/src/components/Search.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import { theme } from "../theme";
|
||||
import { IconSearch, IconX } from "@tabler/icons-react";
|
||||
|
||||
export default function Search({
|
||||
onChange,
|
||||
}: {
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [showClear, setShowClear] = useState(false);
|
||||
const handleChange = (event: ChangeEvent) => {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
setSearchQuery(value);
|
||||
onChange(value);
|
||||
if (value !== "") {
|
||||
setShowClear(true);
|
||||
} else setShowClear(false);
|
||||
};
|
||||
const handleClear = () => {
|
||||
setShowClear(false);
|
||||
setSearchQuery("");
|
||||
onChange("");
|
||||
};
|
||||
return (
|
||||
<div className={`w-full px-6 text-${theme}-500`}>
|
||||
<div
|
||||
className={`flex items-center w-full rounded-md border border-${theme}-200 dark:border-${theme}-700 px-2 gap-1 bg-${theme}-800 flex-nowrap peer`}
|
||||
>
|
||||
<IconSearch className="size-5" />
|
||||
<div className="w-full">
|
||||
<input
|
||||
className={`w-full h-10 text-sm text-${theme}-400 focus:outline-none peer bg-transparent placeholder:text-${theme}-500`}
|
||||
placeholder="Search"
|
||||
onChange={handleChange}
|
||||
value={searchQuery}
|
||||
></input>
|
||||
</div>
|
||||
{showClear && (
|
||||
<button onClick={handleClear} className={`hover:text-${theme}-400`}>
|
||||
<IconX className="size-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="relative -translate-y-[8px] h-[8px] border-b-blue-600 border-b-2 w-0 peer-has-[:focus]:w-full transition-all duration-200 rounded-md left-1/2 -translate-x-1/2"
|
||||
style={{ clipPath: "inset(calc(100% - 2px) 0 0 0)" }}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
web/src/components/Statistic.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
IconCircleArrowUpFilled,
|
||||
IconCircleCheckFilled,
|
||||
IconEyeFilled,
|
||||
IconHelpCircleFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import { theme } from "../theme";
|
||||
|
||||
export default function Statistic({
|
||||
name,
|
||||
value,
|
||||
}: {
|
||||
name: string;
|
||||
value: number;
|
||||
}) {
|
||||
name = name.replaceAll("_", " ");
|
||||
name = name.slice(0, 1).toUpperCase() + name.slice(1); // Capitalize name
|
||||
return (
|
||||
<div
|
||||
className={`before:bg-${theme}-200 before:dark:bg-${theme}-800 after:bg-${theme}-200 after:dark:bg-${theme}-800 gi`}
|
||||
>
|
||||
<div className="xl:px-8 px-6 py-4 gap-y-2 gap-x-4 justify-between align-baseline flex flex-col h-full">
|
||||
<dt
|
||||
className={`text-${theme}-500 dark:text-${theme}-400 leading-6 font-medium`}
|
||||
>
|
||||
{name}
|
||||
</dt>
|
||||
<div className="flex gap-1 justify-between items-center">
|
||||
<dd className="text-black dark:text-white tracking-tight leading-10 font-medium text-3xl w-full">
|
||||
{value}
|
||||
</dd>
|
||||
{name == "Monitored images" && (
|
||||
<IconEyeFilled className="size-6 text-black dark:text-white shrink-0" />
|
||||
)}
|
||||
{name == "Up to date" && (
|
||||
<IconCircleCheckFilled className="size-6 text-green-500 shrink-0" />
|
||||
)}
|
||||
{name == "Update available" && (
|
||||
<IconCircleArrowUpFilled className="size-6 text-blue-500 shrink-0" />
|
||||
)}
|
||||
{name == "Unknown" && (
|
||||
<IconHelpCircleFilled className="size-6 text-gray-500 shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
web/src/components/Tooltip.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Provider, Root, Trigger, Content } from "@radix-ui/react-tooltip";
|
||||
import { cn } from "../utils";
|
||||
import { forwardRef, ReactNode } from "react";
|
||||
import { theme } from "../theme";
|
||||
|
||||
const TooltipContent = forwardRef<
|
||||
React.ElementRef<typeof Content>,
|
||||
React.ComponentPropsWithoutRef<typeof Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
`z-50 overflow-hidden rounded-md border border-${theme}-200 bg-white px-3 py-1.5 text-sm text-${theme}-950 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-${theme}-800 dark:bg-${theme}-950 dark:text-${theme}-50`,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
TooltipContent.displayName = Content.displayName;
|
||||
|
||||
const WithTooltip = ({
|
||||
children,
|
||||
text,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
text: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Provider>
|
||||
<Root>
|
||||
<Trigger className={className} asChild>
|
||||
{children}
|
||||
</Trigger>
|
||||
<TooltipContent>
|
||||
<p className="text-black dark:text-white">{text}</p>
|
||||
</TooltipContent>
|
||||
</Root>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { WithTooltip };
|
||||
54
web/src/index.css
Normal file
@@ -0,0 +1,54 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Kinda hacky, but thank you https://geary.co/internal-borders-css-grid/ */
|
||||
.gi {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gi::before,
|
||||
.gi::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.gi::before {
|
||||
inline-size: 1px;
|
||||
block-size: 100vh;
|
||||
inset-inline-start: -0.125rem;
|
||||
}
|
||||
|
||||
.gi::after {
|
||||
inline-size: 100vw;
|
||||
block-size: 1px;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: -0.12rem;
|
||||
}
|
||||
|
||||
@supports (scrollbar-color: auto) {
|
||||
html {
|
||||
scrollbar-color: #707070 #343840;
|
||||
}
|
||||
}
|
||||
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
html::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-track {
|
||||
background: #343840;
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-thumb {
|
||||
background: #707070;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-thumb:hover {
|
||||
background: #b5b5b5;
|
||||
}
|
||||
}
|
||||
10
web/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
1
web/src/theme.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const theme = "neutral"; // Will be modified by server at runtime
|
||||
10
web/src/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Data {
|
||||
metrics: {
|
||||
monitored_images: number;
|
||||
up_to_date: number;
|
||||
update_available: number;
|
||||
unknown: number;
|
||||
};
|
||||
images: Record<string, boolean | null>;
|
||||
last_updated: string;
|
||||
};
|
||||
6
web/src/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
1
web/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
45
web/tailwind.config.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./src/App.tsx", "./src/components/*.tsx"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
safelist: [
|
||||
// Generate minimum extra CSS
|
||||
{
|
||||
pattern: /bg-(gray|neutral)-50/,
|
||||
},
|
||||
{
|
||||
pattern: /bg-(gray|neutral)-(900|950)/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
{
|
||||
pattern: /bg-(gray|neutral)-200/,
|
||||
variants: ["before", "after"],
|
||||
},
|
||||
{
|
||||
pattern: /bg-(gray|neutral)-800/,
|
||||
variants: ["before:dark", "after:dark"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-400/,
|
||||
variants: ["hover"]
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-500/,
|
||||
variants: ["dark", "placeholder"],
|
||||
},
|
||||
{
|
||||
pattern: /divide-(gray|neutral)-800/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
{
|
||||
pattern: /border-(gray|neutral)-200/,
|
||||
},
|
||||
{
|
||||
pattern: /border-(gray|neutral)-700/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
],
|
||||
};
|
||||
24
web/tsconfig.app.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
web/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
22
web/tsconfig.node.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
17
web/vite.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// https://stackoverflow.com/q/69614671/vite-without-hash-in-filename#75344943
|
||||
output: {
|
||||
entryFileNames: `assets/[name].js`,
|
||||
chunkFileNames: `assets/[name].js`,
|
||||
assetFileNames: `assets/[name].[ext]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||