m/cup
1
0
mirror of https://github.com/sergi0g/cup.git synced 2025-11-10 14:13:49 -05:00

27 Commits

Author SHA1 Message Date
Sergio
60096792a9 chore(docs): update JSON API reference schema 2025-03-11 17:08:42 +02:00
Sergio
a599d4e084 feat: detect image url from label 2025-03-11 17:06:00 +02:00
Sergio
a5a1f12899 chore: remove unneeded file 2025-03-11 17:05:46 +02:00
Sergio
766a20ccac chore: add badges to readme 2025-03-11 16:32:59 +02:00
Sergio
fe66ba842a docs: add docs about refresh endpoint 2025-03-11 16:26:48 +02:00
Sergio
c06c20394f docs: add page about home assistant integration 2025-03-11 16:26:31 +02:00
Sergio
98dafb8ba4 chore: bump project version 2025-03-10 16:49:07 +02:00
Sergio
2addfca1b4 fix: code block component copying was working incorrectly (#62) 2025-03-10 16:47:47 +02:00
dependabot[bot]
e3b05923ae Bump ring from 0.17.11 to 0.17.13 (#60)
Bumps [ring](https://github.com/briansmith/ring) from 0.17.11 to 0.17.13.
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-07 19:26:28 +02:00
Sergio
aa4195f8d6 chore: bump project version 2025-03-07 18:25:00 +02:00
Sergio
1b94629c79 fix: overflowing image references in web UI 2025-03-07 18:01:32 +02:00
Sergio
8cd9cce94e chore: bump project version 2025-03-07 17:38:20 +02:00
Sergio
ddabd8c102 fix: strip hash when parsing image references 2025-03-07 17:37:56 +02:00
Sergio
0b0028ab6d Fix search in docs 2025-03-07 16:40:59 +02:00
Sergio
75509550b1 Fix CI Dockerfile 2025-03-03 11:35:58 +02:00
Sergio
9716d1a351 Fix CI Dockerfile 2025-03-03 11:30:09 +02:00
Sergio
d5a2556768 Fix CI workflows 2025-03-03 11:23:26 +02:00
Sergio
e7f1921620 I'm stupid 2025-03-03 11:14:16 +02:00
Sergio
7ea6ae6de5 Let's try again 2025-03-03 10:31:42 +02:00
Sergio
d7c2e6436c Fix CI workflows 2025-03-03 10:23:08 +02:00
Sergio
fde61ee07d Fix CI workflows 2025-03-03 10:18:11 +02:00
Sergio
c4de3961a0 Optimize CI for docker image 2025-03-03 10:05:18 +02:00
Sergio
404c574c2c Update Rust version in Dockerfile 2025-03-02 13:08:43 +02:00
Sergio
6d4df20f54 Ignore registries before retrieving auth tokens 2025-03-02 13:05:23 +02:00
Sergio
7b3745d095 Fix errors and revert reqwest-middleware to v0.3.3 2025-03-02 13:04:31 +02:00
Sergio
f9aa516da7 Update dependencies. Also fixes a security vulnerability in rustls 2025-03-02 12:45:29 +02:00
Sergio
0f9c5d1466 V3
Many many many changes, honestly just read the release notes
2025-02-28 20:43:49 +02:00
62 changed files with 1280 additions and 1059 deletions

12
.github/actions/build-image/Dockerfile vendored Normal file
View File

@@ -0,0 +1,12 @@
FROM --platform=$BUILDPLATFORM alpine AS builder
ARG TARGETARCH
ARG TARGETOS
COPY binaries/* /
RUN mv cup-$TARGETOS-$TARGETARCH cup
RUN chmod +x cup
FROM scratch
COPY --from=builder /cup /cup
ENTRYPOINT ["/cup"]

52
.github/actions/build-image/action.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: Build Image
inputs:
tags:
description: "Docker image tags"
required: true
gh-token:
description: "Github token"
required: true
runs:
using: "composite"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download binaries
uses: actions/download-artifact@v4
with:
path: .
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/sergi0g/cup
tags: ${{ inputs.tags }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: sergi0g
password: ${{ inputs.gh-token }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: ./.github/actions/build-image/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -36,7 +36,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- name: Set up Bun - name: Set up Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
@@ -62,38 +62,24 @@ jobs:
cup-linux-arm64 cup-linux-arm64
build-image: build-image:
needs: get-tag needs:
- get-tag
- build-binaries
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: ./.github/actions/build-image
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with: with:
registry: ghcr.io tags: |
username: ${{ github.repository_owner }} ${{ needs.get-tag.outputs.tag }}
password: ${{ secrets.GITHUB_TOKEN }} gh-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64, linux/arm64
push: true
tags: ghcr.io/sergi0g/cup:${{ needs.get-tag.outputs.tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
nightly-release: nightly-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build-binaries, get-tag] needs:
- build-binaries
- build-image
steps: steps:
- name: Download binaries - name: Download binaries
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4

View File

@@ -60,34 +60,19 @@ jobs:
cup-linux-arm64 cup-linux-arm64
build-image: build-image:
needs: get-tag needs:
- get-tag
- build-binaries
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: ./.github/actions/build-image
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with: with:
registry: ghcr.io tags: |
username: ${{ github.repository_owner }} ${{ needs.get-tag.outputs.tag }}
password: ${{ secrets.GITHUB_TOKEN }} latest
gh-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64, linux/arm64
push: true
tags: ghcr.io/sergi0g/cup:${{ needs.get-tag.outputs.tag }},ghcr.io/sergi0g/cup:latest
cache-from: type=gha
cache-to: type=gha,mode=max
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -107,4 +92,4 @@ jobs:
prerelease: true prerelease: true
tag_name: ${{ needs.get-tag.outputs.tag }} tag_name: ${{ needs.get-tag.outputs.tag }}
name: ${{ needs.get-tag.outputs.tag }} name: ${{ needs.get-tag.outputs.tag }}
files: binaries/* files: binaries/*

1167
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,15 @@
[package] [package]
name = "cup" name = "cup"
version = "3.0.0" version = "3.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.5.7", features = ["derive"] } clap = { version = "4.5.7", features = ["derive"] }
indicatif = { version = "0.17.8", optional = true } indicatif = { version = "0.17.8", optional = true }
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
xitca-web = { version = "0.5.0", optional = true } xitca-web = { version = "0.6.2", optional = true }
liquid = { version = "0.26.6", optional = true } liquid = { version = "0.26.6", optional = true }
bollard = "0.16.1" bollard = "0.18.1"
once_cell = "1.19.0" once_cell = "1.19.0"
http-auth = { version = "0.1.9", default-features = false } http-auth = { version = "0.1.9", default-features = false }
termsize = { version = "0.1.8", optional = true } termsize = { version = "0.1.8", optional = true }
@@ -17,11 +17,11 @@ regex = { version = "1.10.5", default-features = false, features = ["perf"] }
chrono = { version = "0.4.38", default-features = false, features = ["std", "alloc", "clock"], optional = true } chrono = { version = "0.4.38", default-features = false, features = ["std", "alloc", "clock"], optional = true }
reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls"] } reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls"] }
futures = "0.3.30" futures = "0.3.30"
reqwest-retry = "0.6.1" reqwest-retry = "0.7.0"
reqwest-middleware = "0.3.3" reqwest-middleware = "0.3.3"
rustc-hash = "2.0.0" rustc-hash = "2.0.0"
http-link = "1.0.1" http-link = "1.0.1"
itertools = "0.13.0" itertools = "0.14.0"
serde_json = "1.0.133" serde_json = "1.0.133"
serde = "1.0.215" serde = "1.0.215"
tokio-cron-scheduler = { version = "0.13.0", default-features = false, optional = true } tokio-cron-scheduler = { version = "0.13.0", default-features = false, optional = true }

View File

@@ -15,7 +15,7 @@ RUN ~/.bun/bin/bun install
RUN ~/.bun/bin/bun run build RUN ~/.bun/bin/bun run build
### Build Cup ### ### Build Cup ###
FROM rust:1.80.1-alpine AS build FROM rust:1-alpine AS build
# Requirements # Requirements
RUN apk add musl-dev RUN apk add musl-dev

View File

@@ -1,5 +1,13 @@
# Cup 🥤 # Cup 🥤
![GitHub License](https://img.shields.io/github/license/sergi0g/cup)
![CI Status](https://img.shields.io/github/actions/workflow/status/sergi0g/cup/.github%2Fworkflows%2Fci.yml?label=CI)
![GitHub last commit](https://img.shields.io/github/last-commit/sergi0g/cup)
![GitHub Release](https://img.shields.io/github/v/release/sergi0g/cup)
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/sergi0g/cup)
![Discord](https://img.shields.io/discord/1337705080518086658)
Cup is the easiest way to check for container image updates. Cup is the easiest way to check for container image updates.
![Cup web in dark mode](screenshots/web_dark.png) ![Cup web in dark mode](screenshots/web_dark.png)
@@ -17,18 +25,18 @@ _If you like this project and/or use Cup, please consider starring the project
- Supports most registries, including Docker Hub, ghcr.io, Quay, lscr.io and even Gitea (or derivatives) - 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/getwud/wud) which would always use it up. - 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/getwud/wud) which would always use it up.
- Beautiful CLI and web interface for checking on your containers any time. - Beautiful CLI and web interface for checking on your containers any time.
- The binary is tiny! At the time of writing it's just 5.1 MB. No more pulling 100+ MB docker images for a such a simple program. - The binary is tiny! At the time of writing it's just 5.4 MB. No more pulling 100+ MB docker images for a such a simple program.
- JSON output for both the CLI and web interface so you can connect Cup to integrations. It's easy to parse and makes webhooks and pretty dashboards simple to set up! - JSON output for both the CLI and web interface so you can connect Cup to integrations. It's easy to parse and makes webhooks and pretty dashboards simple to set up!
## Documentation 📘 ## Documentation 📘
Take a look at https://sergi0g.github.io/cup/docs! Take a look at https://cup.sergi0g.dev/docs!
## Limitations ## Limitations
Cup is a work in progress. It might not have as many features as other alternatives. If one of these features is really important for you, please consider using another tool. Cup is a work in progress. It might not have as many features as other alternatives. If one of these features is really important for you, please consider using another tool.
- Cup cannot directly 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). - Cup cannot directly 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 `/api/v3/json` url from the server).
## Roadmap ## Roadmap
Take a sneak peek at what's coming up in future releases on the [roadmap](https://github.com/users/sergi0g/projects/2)! Take a sneak peek at what's coming up in future releases on the [roadmap](https://github.com/users/sergi0g/projects/2)!
@@ -44,7 +52,7 @@ Here are some ideas to get you started:
- Help optimize Cup and make it even better! - Help optimize Cup and make it even better!
- Add more features to the web UI - Add more features to the web UI
For more information, check the [docs](https://sergi0g.github.io/cup/docs/contributing)! For more information, check the [docs](https://cup.sergi0g.dev/docs/contributing)!
## Support ## Support

Binary file not shown.

View File

@@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build && pagefind --site out --output-path out/_pagefind",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"fmt": "bun prettier --write ." "fmt": "bun prettier --write ."
@@ -26,6 +26,7 @@
"@types/react-dom": "^19.0.3", "@types/react-dom": "^19.0.3",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-config-next": "15.1.5", "eslint-config-next": "15.1.5",
"pagefind": "^1.3.0",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -1,15 +0,0 @@
import React from "react";
export function GitHubIcon({ className }: { className?: string | undefined }) {
return (
<svg
width="24"
height="24"
fill="currentColor"
viewBox="3 3 18 18"
className={className}
>
<path d="M12 3C7.0275 3 3 7.12937 3 12.2276C3 16.3109 5.57625 19.7597 9.15374 20.9824C9.60374 21.0631 9.77249 20.7863 9.77249 20.5441C9.77249 20.3249 9.76125 19.5982 9.76125 18.8254C7.5 19.2522 6.915 18.2602 6.735 17.7412C6.63375 17.4759 6.19499 16.6569 5.8125 16.4378C5.4975 16.2647 5.0475 15.838 5.80124 15.8264C6.51 15.8149 7.01625 16.4954 7.18499 16.7723C7.99499 18.1679 9.28875 17.7758 9.80625 17.5335C9.885 16.9337 10.1212 16.53 10.38 16.2993C8.3775 16.0687 6.285 15.2728 6.285 11.7432C6.285 10.7397 6.63375 9.9092 7.20749 9.26326C7.1175 9.03257 6.8025 8.08674 7.2975 6.81794C7.2975 6.81794 8.05125 6.57571 9.77249 7.76377C10.4925 7.55615 11.2575 7.45234 12.0225 7.45234C12.7875 7.45234 13.5525 7.55615 14.2725 7.76377C15.9937 6.56418 16.7475 6.81794 16.7475 6.81794C17.2424 8.08674 16.9275 9.03257 16.8375 9.26326C17.4113 9.9092 17.76 10.7281 17.76 11.7432C17.76 15.2843 15.6563 16.0687 13.6537 16.2993C13.98 16.5877 14.2613 17.1414 14.2613 18.0065C14.2613 19.2407 14.25 20.2326 14.25 20.5441C14.25 20.7863 14.4188 21.0746 14.8688 20.9824C16.6554 20.364 18.2079 19.1866 19.3078 17.6162C20.4077 16.0457 20.9995 14.1611 21 12.2276C21 7.12937 16.9725 3 12 3Z"></path>
</svg>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -10,12 +10,12 @@ export function Card({
description: string; description: string;
}) { }) {
return ( return (
<div> <div className="p-4 bg-white dark:bg-black group">
<Icon className="text-black size-7 dark:text-white inline mr-2" /> <Icon className="text-black size-7 group-hover:size-9 dark:text-white inline mr-2 transition-[width,height] duration-200" />
<span className="align-middle text-2xl font-bold text-black dark:text-white"> <span className="align-middle text-2xl font-bold text-black dark:text-white">
{name} {name}
</span> </span>
<p className="text-2xl font-semibold text-neutral-500 dark:text-neutral-500"> <p className="text-xl font-semibold text-neutral-500 dark:text-neutral-500">
{description} {description}
</p> </p>
</div> </div>

View File

@@ -1,28 +0,0 @@
"use client";
import { IconCopy, IconCopyCheck } from "@tabler/icons-react";
import { useState } from "react";
export default function CopyableCode({ children }: { children: string }) {
const [success, setSuccess] = useState(false);
const handleClick = () => {
navigator.clipboard.writeText(children);
setSuccess(true);
setTimeout(() => setSuccess(false), 3000);
};
return (
<div className="relative rounded-md xl:w-auto">
<button
className="hover:bg-black/10 dark:hover:bg-black/60 flex w-full items-center justify-center gap-4 rounded-md border border-black/10 bg-black/5 px-8 py-3 font-mono text-sm font-medium text-black/70 transition-colors duration-200 md:px-10 md:py-3 md:text-base md:leading-6 dark:border-white/15 dark:bg-black dark:text-gray-300 backdrop-blur-md"
onClick={handleClick}
>
{children}
{success ? (
<IconCopyCheck className="stroke-black/40 dark:stroke-white/50" />
) : (
<IconCopy className="stroke-black/40 dark:stroke-white/50" />
)}
</button>
</div>
);
}

View File

@@ -8,7 +8,7 @@ export function GridPattern() {
return ( return (
<svg <svg
aria-hidden="true" aria-hidden="true"
className="pointer-events-none absolute inset-0 bottom-0 left-0 right-0 top-0 -z-10 h-full w-full stroke-neutral-200 dark:stroke-neutral-600/30" className="pointer-events-none absolute inset-0 bottom-0 left-0 right-0 top-0 h-full w-full -z-10 bg-white stroke-neutral-200 dark:stroke-white/10 dark:bg-black"
> >
<defs> <defs>
<pattern <pattern
@@ -22,7 +22,6 @@ export function GridPattern() {
<path <path
d={`M.5 ${SIZE}V.5H${SIZE}`} d={`M.5 ${SIZE}V.5H${SIZE}`}
fill="none" fill="none"
strokeDasharray={"4 2"}
/> />
</pattern> </pattern>
</defs> </defs>

View File

@@ -1,28 +0,0 @@
import { ReactNode } from "react";
import { GradientText } from "./GradientText";
export function Section({
title,
className,
children,
}: {
title: string;
className: string;
children: ReactNode;
}) {
return (
<div className="border-t border-t-neutral-300 bg-neutral-50 py-32 dark:border-t-neutral-600/30 dark:bg-neutral-950">
<div className="mx-auto w-full max-w-screen-xl">
<GradientText
text={title}
className="mx-auto mb-20 w-fit text-center text-4xl font-bold tracking-tighter"
innerClassName={className}
blur={12}
/>
<div className="m-2 grid w-full auto-cols-fr gap-20 lg:grid-cols-3">
{children}
</div>
</div>
</div>
);
}

View File

@@ -1,29 +1,28 @@
import React from "react"; import React from "react";
import "./styles.css" import "./styles.css";
import CopyableCode from "../CopyableCode";
import { Browser } from "../Browser"; import { Browser } from "../Browser";
import { Card } from "../Card"; import { Card } from "../Card";
import { import {
IconAdjustments, IconAdjustments,
IconArrowRight, IconArrowRight,
IconBarrierBlockOff,
IconBolt, IconBolt,
IconBraces,
IconDevices,
IconFeather, IconFeather,
IconLockCheck, IconGitMerge,
IconPuzzle,
IconServer,
IconTerminal,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { GitHubIcon } from "nextra/icons"; import { GitHubIcon } from "nextra/icons";
import { GridPattern } from "../GridPattern"; import { GridPattern } from "../GridPattern";
import { GradientText } from "../GradientText"; import { GradientText } from "../GradientText";
import { Section } from "../Section";
import { Steps } from "nextra/components";
import Link from "next/link"; import Link from "next/link";
export default async function Home() { export default async function Home() {
return ( return (
<> <>
<div className="relative home"> <div className="relative home bg-radial-[ellipse_at_center] from-transparent from-20% to-white dark:to-black">
<GridPattern /> <GridPattern />
<div className="px-4 pt-16 pb-8 sm:pt-24 lg:px-8"> <div className="px-4 pt-16 pb-8 sm:pt-24 lg:px-8">
<div className="flex w-full flex-col items-center justify-between"> <div className="flex w-full flex-col items-center justify-between">
@@ -37,7 +36,7 @@ export default async function Home() {
blur={30} blur={30}
/> />
</h1> </h1>
<h3 className="mx-auto mt-6 max-w-3xl text-center text-xl leading-tight font-medium text-gray-400"> <h3 className="mx-auto mt-6 max-w-3xl text-center text-xl leading-tight font-medium text-neutral-500 dark:text-neutral-400">
Cup is a small utility with a big impact. Simplify your Cup is a small utility with a big impact. Simplify your
container management workflow with fast and efficient update container management workflow with fast and efficient update
checking, a full-featured CLI and web interface, and more. checking, a full-featured CLI and web interface, and more.
@@ -54,7 +53,7 @@ export default async function Home() {
<a <a
href="https://github.com/sergi0g/cup" href="https://github.com/sergi0g/cup"
target="_blank" target="_blank"
className="hide-focus h-full text-nowrap border border-neutral-400 transition-colors duration-200 ease-in-out hover:border-neutral-600 focus:border-neutral-600 dark:border-neutral-600 hover:dark:border-neutral-400 hover:dark:shadow-sm hover:dark:shadow-neutral-600 focus:dark:border-neutral-400" className="hide-focus h-full bg-white dark:bg-black text-nowrap border border-black/15 transition-colors duration-200 ease-in-out hover:border-black/40 dark:border-white/15 hover:dark:border-white/40 hover:dark:shadow-sm focus:dark:border-white/30"
> >
Star on GitHub Star on GitHub
<GitHubIcon className="ml-auto size-4 md:size-5" /> <GitHubIcon className="ml-auto size-4 md:size-5" />
@@ -66,68 +65,49 @@ export default async function Home() {
<Browser /> <Browser />
</div> </div>
</div> </div>
<Section <div className="bg-white dark:bg-black py-12 px-8 w-full">
title="Powerful at its core." <div className="flex h-full w-full items-center justify-center">
className="bg-gradient-to-r from-red-500 to-amber-500" <div className="grid md:grid-cols-2 md:grid-rows-4 lg:grid-cols-4 lg:grid-rows-2 w-full max-w-7xl gap-px border border-transparent bg-black/10 dark:bg-white/10">
> <Card
<Card name="Built for speed."
name="100% Safe Code" icon={IconBolt}
icon={IconLockCheck} description="Cup is written in Rust and every release goes through extensive profiling to squeeze out every last drop of performance."
description="Built with safe Rust and Typescript to ensure security and reliability." />
/> <Card
<Card name="Configurable."
name="Lightning Fast Performance" icon={IconAdjustments}
icon={IconBolt} description="Make Cup yours with the extensive configuration options available. Customize and tailor it to your needs."
description="Heavily optimized to squeeze out every last drop of performance. Each release is extensively benchmarked and profiled so that you'll never have to stare at a loading spinner for long." />
/> <Card
<Card name="Extend it."
name="Lightweight" icon={IconPuzzle}
icon={IconFeather} description="JSON output enables you to connect Cup with your favorite integrations, build automations and more."
description="No runtimes or libraries are needed. All you need is the 5.1 MB static binary that works out of the box on any system." />
/> <Card
</Section> name="CLI available."
<Section icon={IconTerminal}
title="Efficient, yet flexible." description="Do you like terminals? Cup has a CLI. Check for updates quickly without spinning up a server."
className="bg-gradient-to-r from-blue-500 to-indigo-500" />
> <Card
<Card name="Multiple servers."
name="JSON output" icon={IconServer}
description="Connect Cup to your favorite intergrations with JSON output for the CLI and an API for the server. Now go make that cool dashboard you've been dreaming of!" description="Run multiple Cup instances and effortlessly check on them through one web interface."
icon={IconBraces} />
/> <Card
<Card name="Unstoppable."
name="Both CLI and web interface" icon={IconBarrierBlockOff}
description="Whether you prefer the command line or the web, Cup runs wherever you choose." description="Cup is designed to check for updates without using up any rate limits. 10 images per hour won't be a problem, even with 100 images."
icon={IconDevices} />
/> <Card
<Card name="Lightweight."
name="Configurable" icon={IconFeather}
description="The simple configuration file provides you with all the tools you need to specify a custom Docker socket, manage registry connection options, choose a theme for the web interface and more." description="No need for a powerful server and endless storage. The tiny 5.4 MB binary won't hog your CPU and memory."
icon={IconAdjustments} />
/> <Card
</Section> name="Open source."
<div className="relative py-24 border-t border-t-neutral-300 dark:border-t-neutral-600/30 text-black dark:text-white"> icon={IconGitMerge}
<GridPattern /> description="All source code is publicly available in our GitHub repository. We're looking for contributors!"
<div className="mx-auto flex w-full max-w-screen-xl flex-col items-center"> />
<p className="mb-8 text-center text-3xl font-bold">
Still not convinced? Try it out now!
</p>
<div>
<Steps>
<h3 className="mb-2">Open a terminal and run</h3>
<CopyableCode>
docker run --rm -t -v /var/run/docker.sock:/var/run/docker.sock
-p 8000:8000 ghcr.io/sergi0g/cup serve
</CopyableCode>
<h3 className="mb-2">Open the dashboard in your browser</h3>
<p>
Visit{" "}
<a href="http://localhost:8000" className="underline">
http://localhost:8000
</a>{" "}
in your browser to try it out!
</p>
</Steps>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -20,7 +20,7 @@ const logo = (
); );
const navbar = ( const navbar = (
<Navbar logo={logo} projectLink="https://github.com/sergi0g/cup"> <Navbar logo={logo} projectLink="https://github.com/sergi0g/cup" chatLink="https://discord.gg/jmh5ctzwNG">
<ThemeSwitch lite className="cursor-pointer" /> <ThemeSwitch lite className="cursor-pointer" />
</Navbar> </Navbar>
); );

View File

@@ -35,9 +35,9 @@ services:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
``` ```
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. Cup can run with a non-root user, but needs to be in a docker group. Assuming user id of 1000 and `docker` group id of 999 you can add this to the `services.cup` key in the docker compose:
Cup can run with a non-root user, but needs to be in a docker group. Assuming user id of 1000 and `docker` group id of 999 you can add this to the docker compose:
```yaml ```yaml
user: "1000:999" user: "1000:999"
``` ```
The compose 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!

View File

@@ -0,0 +1,47 @@
import Image from "next/image";
import screenshot from "@/app/assets/ha-cup-component.png";
# Home Assistant integration
Many thanks to [@bastgau](https://github.com/bastgau) for creating this integration.
## About
The **HA Cup Component** integration for Home Assistant allows you to retrieve update statistics for Docker containers directly from your Home Assistant interface.
With this integration, you can easily track the status of your Docker containers and receive notifications when updates are available.
The following sensors are currently implemented:
<Image
src={screenshot}
alt="Screenshot of Home Assistant showing a card with update information provided by Cup"
/>
## Installation
### Via HACS
1. Open Home Assistant and go to HACS
2. Navigate to "Integrations" and click on "Add a custom repository".
3. Use https://github.com/bastgau/ha-cup-component as the URL
4. Search for "HA Cup Component" and install it.
5. Restart Home Assistant.
### One-click install
[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=bastgau&repository=ha-cup-component&category=Integration)
### Manual Installation
1. Download the integration files from the GitHub repository.
2. Place the integration folder in the custom_components directory of Home Assistant.
3. Restart Home Assistant.
## Support & Contributions
If you encounter any issues or wish to contribute to improving this integration, feel free to open an issue or a pull request in the [GitHub repository](https://github.com/bastgau/ha-cup-component).
Support the author:
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/bastgau)

View File

@@ -36,7 +36,7 @@ services:
homepage.widget.mappings[1].field.metrics: up_to_date homepage.widget.mappings[1].field.metrics: up_to_date
homepage.widget.mappings[1].format: number homepage.widget.mappings[1].format: number
homepage.widget.mappings[2].label: Updates homepage.widget.mappings[2].label: Updates
homepage.widget.mappings[2].field.metrics: update_available homepage.widget.mappings[2].field.metrics: updates_available
homepage.widget.mappings[2].format: number homepage.widget.mappings[2].format: number
``` ```
@@ -64,7 +64,7 @@ widget:
label: Up to date label: Up to date
format: number format: number
- field: - field:
metrics: update_available metrics: updates_available
label: Available updates label: Available updates
format: number format: number
- field: - field:

View File

@@ -0,0 +1,12 @@
# Agent mode
If you'd like to have only the server API exposed without the dashboard, you can run Cup in agent mode.
Modify your config like this:
```jsonc
{
"agent": true
// Other options
}
```

View File

@@ -4,7 +4,7 @@ import { Callout } from "nextra/components";
Some registries (or specific images) may require you to be authenticated. For those, you can modify `cup.json` like this: Some registries (or specific images) may require you to be authenticated. For those, you can modify `cup.json` like this:
```json ```jsonc
{ {
"registries": { "registries": {
"<YOUR_REGISTRY_DOMAIN_1>": { "<YOUR_REGISTRY_DOMAIN_1>": {

View File

@@ -0,0 +1,12 @@
# Automatic refresh
Cup can automatically refresh the results when running in server mode. Simply add this to your config:
```jsonc
{
"refresh_interval": "0 0,30 * 0 0" // Check twice an hour
// Other options
}
```
You can use a cron expression to specify the refresh interval. The reference is [here](https://github.com/Hexagon/croner-rust#pattern)

View File

@@ -0,0 +1,22 @@
# Ignored registries
If you want to skip checking images from some registries, you can modify your config like this:
```jsonc
{
"registries": {
"<SOME_REGISTRY_DOMAIN_1>": {
"ignore": true
// Other options
},
"<SOME_REGISTRY_DOMAIN_2>" {
"ignore": false
// Other options
},
// ...
}
// Other options
}
```
This configuration option is a bit redundant, since you can achieve the same with [this option](/docs/configuration/include-exclude-images). It's recommended to use that.

View File

@@ -0,0 +1,35 @@
# Include/Exclude images
If you want to exclude some images (e.g. because they have too many tags and take too long to check), you can add the following to your config:
```jsonc
{
"images": {
"exclude": [
"ghcr.io/immich-app/immich-machine-learning",
"postgres:15"
]
// ...
}
// Other options
}
```
For an image to be excluded, it must start with one of the strings you specify above. That means you could use `ghcr.io` to exclude all images from ghcr.io or `ghcr.io/sergi0g` to exclude all my images (why would you do that?).
If you want Cup to always check some extra images that aren't available locally, you can modify your config like this:
```jsonc
{
"images": {
"extra": [
"mysql:8.0",
"nextcloud:30"
]
// ...
}
// Other options
}
```
Note that you must specify images with version tags, otherwise Cup will exit with an error!

View File

@@ -8,7 +8,7 @@ import {
IconLockOpen, IconLockOpen,
IconKey, IconKey,
IconPlug, IconPlug,
IconServer IconServer,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
# Configuration # Configuration
@@ -71,12 +71,21 @@ Here's a full example:
```json ```json
{ {
"authentication": { "$schema": "https://raw.githubusercontent.com/sergi0g/cup/main/cup.schema.json",
"ghcr.io": "<YOUR_TOKEN_HERE>", "version": 3,
"registry-1.docker.io": "<YOUR_TOKEN_HERE>" "images": {
"exclude": ["ghcr.io/immich-app/immich-machine-learning"],
"extra": ["ghcr.io/sergi0g/cup:v3.0.0"]
}, },
"theme": "blue", "registries": {
"insecure_registries": ["localhost:5000", "my-insecure-registry.example.com"] "myregistry.com": {
"authentication": "<YOUR_TOKEN_HERE>"
}
},
"servers": {
"Raspberry Pi": "https://server.local:8000"
},
"theme": "blue"
} }
``` ```

View File

@@ -8,7 +8,7 @@ To solve this problem, you can specify exceptions in your `cup.json`.
Here's what it looks like: Here's what it looks like:
```json ```jsonc
{ {
"registries": { "registries": {
"<INSECURE_REGISTRY_1>": { "<INSECURE_REGISTRY_1>": {

View File

@@ -4,7 +4,7 @@ Besides checking for local image updates, you might want to be able to view upda
Just add something like this to your config: Just add something like this to your config:
```json ```jsonc
{ {
"servers": { "servers": {
"Cool server 1": "http://your-other-server-running-cup:8000", "Cool server 1": "http://your-other-server-running-cup:8000",

View File

@@ -1,8 +1,8 @@
# Socket # Custom socket
If you need to specify a custom Docker socket (e.g. because you're using Podman), you can use the `socket` option. Here's an example: If you need to specify a custom Docker socket (e.g. because you're using Podman), you can use the `socket` option. Here's an example:
```json ```jsonc
{ {
"socket": "/run/user/1000/podman/podman.sock" "socket": "/run/user/1000/podman/podman.sock"
// Other options // Other options
@@ -11,7 +11,7 @@ If you need to specify a custom Docker socket (e.g. because you're using Podman)
You can also specify a TCP socket if you're using a remote Docker host or a [proxy](https://github.com/Tecnativa/docker-socket-proxy): You can also specify a TCP socket if you're using a remote Docker host or a [proxy](https://github.com/Tecnativa/docker-socket-proxy):
```json ```jsonc
{ {
"socket": "tcp://localhost:2375" "socket": "tcp://localhost:2375"
// Other options // Other options

View File

@@ -21,7 +21,7 @@ Available options are `default` and `blue`.
Here's an example: Here's an example:
```json ```jsonc
{ {
"theme": "blue" "theme": "blue"
// Other options // Other options

View File

@@ -15,7 +15,7 @@ Cup is a lightweight alternative to [What's up Docker?](https://github.com/getwu
- Supports most registries, including Docker Hub, ghcr.io, Quay, lscr.io and even Gitea (or derivatives) - 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/getwud/wud) which would always use it up. - 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/getwud/wud) which would always use it up.
- Beautiful CLI and web interface for checking on your containers any time. - Beautiful CLI and web interface for checking on your containers any time.
- The binary is tiny! At the time of writing it's just 5.1 MB. No more pulling 100+ MB docker images for a such a simple program. - The binary is tiny! At the time of writing it's just 5.4 MB. No more pulling 100+ MB docker images for a such a simple program.
- JSON output for both the CLI and web interface so you can connect Cup to integrations. It's easy to parse and makes webhooks and pretty dashboards simple to set up! - JSON output for both the CLI and web interface so you can connect Cup to integrations. It's easy to parse and makes webhooks and pretty dashboards simple to set up!
# Installation # Installation

View File

@@ -1,5 +1,5 @@
import { Callout, Cards } from "nextra/components"; import { Callout, Cards } from "nextra/components";
import { IconServer, IconTerminal } from "@tabler/icons-react" import { IconServer, IconTerminal } from "@tabler/icons-react";
# Integrations # Integrations
@@ -34,6 +34,7 @@ The data returned from the API or from the CLI is in JSON and looks like this:
"repository": "sergi0g/cup", "repository": "sergi0g/cup",
"tag": "latest", "tag": "latest",
}, },
"url": "https://github.com/sergi0g/cup", // The URL specified in the "org.opencontainers.image.url" label, otherwise null
"result": { "result": {
"has_update": true, // `true` when an image has an update of any kind, `false` when up to date and `null` when unknown. "has_update": true, // `true` when an image has an update of any kind, `false` when up to date and `null` when unknown.
"info": { "info": {
@@ -77,3 +78,7 @@ For retrieving the above data, refer to the CLI and server pages:
href="/docs/usage/server" href="/docs/usage/server"
/> />
</Cards> </Cards>
## Refresh Cup
If you'd like to fetch the latest information, you can manually trigger a refresh by making a `GET` request to the `/api/v3/refresh` endpoint. Once the request completes, you can fetch the data as described above.

View File

@@ -12,36 +12,52 @@ Cup's CLI provides the `cup check` command.
```ansi ```ansi
$ cup check $ cup check
 ✓ Done!
mysql:8.0 Major update ~ Local images
node:20 Major update ╭─────────────────────────────────────────┬──────────────────────────────────┬─────────╮
postgres:16-alpine Major update │Reference │Status │Time (ms)│
rust:1.80.1-alpine Minor update ├─────────────────────────────────────────┼──────────────────────────────────┼─────────┤
redis:7.4.0 Patch update │postgres:15-alpine │Major update (15 → 17) │788 │
nginx:alpine Update available │ghcr.io/immich-app/immich-server:v1.118.2│Minor update (1.118.2 → 1.127.0) │2294 │
redis:alpine Update available │ollama/ollama:0.4.1 │Minor update (0.4.1 → 0.5.12) │533 │
ubuntu:latest Update available │adguard/adguardhome:v0.107.52 │Patch update (0.107.52 → 0.107.57)│1738 │
node:iron Up to date │jc21/nginx-proxy-manager:latest │Up to date │583 │
2fauth/2fauth:latest Up to date │louislam/uptime-kuma:1 │Up to date │793 │
c1982/sdns:latest Up to date │moby/buildkit:buildx-stable-1 │Up to date │600 │
registry.acme.com/acme-server:latest Unknown │tecnativa/docker-socket-proxy:latest │Up to date │564 │
INFO ✨ Checked 58 images in 3772ms ubuntu:latest │Up to date │585 │
│wagoodman/dive:latest │Up to date │585 │
│rolebot:latest │Unknown │174 │
╰─────────────────────────────────────────┴──────────────────────────────────┴─────────╯
 INFO ✨ Checked 11 images in 8312ms
``` ```
### Check for updates to specific images ### Check for updates to specific images
```ansi ```ansi
$ cup check node:latest $ cup check node:latest
node:latest Update available ✓ Done!
INFO ✨ Checked 1 images in 1310ms ~ Local images
╭───────────┬────────────────┬─────────╮
│Reference │Status │Time (ms)│
├───────────┼────────────────┼─────────┤
│node:latest│Update available│788 │
╰───────────┴────────────────┴─────────╯
 INFO ✨ Checked 1 images in 310ms
``` ```
```ansi ```ansi
$ cup check nextcloud:30 postgres:14 mysql:8.0 $ cup check nextcloud:30 postgres:14 mysql:8.0
nextcloud:30 Update available ✓ Done!
postgres:14 Update available ~ Local images
mysql:8.0 Up to date ╭────────────┬────────────────────────┬─────────╮
INFO ✨ Checked 3 images in 1769ms │Reference │Status │Time (ms)│
├────────────┼────────────────────────┼─────────┤
│postgres:14 │Major update (14 → 17) │195 │
│mysql:8.0 │Major update (8.0 → 9.2)│382 │
│nextcloud:30│Up to date │585 │
╰────────────┴────────────────────────┴─────────╯
 INFO ✨ Checked 3 images in 769ms
``` ```
## Enable icons ## Enable icons
@@ -62,7 +78,8 @@ $ cup check -r
``` ```
<Callout emoji="⚠️"> <Callout emoji="⚠️">
When parsing Cup's output, capture only `stdout`, otherwise you might not get valid JSON (if there are warnings) When parsing Cup's output, capture only `stdout`, otherwise you might not get
valid JSON (if there are warnings)
</Callout> </Callout>
## Usage with Docker ## Usage with Docker

View File

@@ -7,7 +7,7 @@ import { Cards } from "nextra/components";
# Usage # Usage
You can use Cup in 2 different ways. As a CLI or as a server. You can learn more about each mode in its corresponding page You can use Cup in 2 different ways. As a CLI or as a server. You can learn more about each mode on its corresponding page
<Cards> <Cards>
<Cards.Card icon={<IconTerminal />} title="CLI" href="/docs/usage/cli" /> <Cards.Card icon={<IconTerminal />} title="CLI" href="/docs/usage/cli" />

View File

@@ -8,13 +8,13 @@ The server provides the `cup serve` command.
```ansi ```ansi
$ cup serve $ cup serve
INFO Starting server, please wait...  INFO Starting server, please wait...
INFO ✨ Checked 8 images in 8862ms  INFO ✨ Checked 8 images in 8862ms
INFO Ready to start!  INFO Ready to start!
HTTP GET / 200 in 0ms  HTTP GET / 200 in 0ms
HTTP GET /assets/index.js 200 in 3ms  HTTP GET /assets/index.js 200 in 3ms
HTTP GET /assets/index.css 200 in 0ms  HTTP GET /assets/index.css 200 in 0ms
HTTP GET /api/v3/json 200 in 0ms  HTTP GET /api/v3/json 200 in 0ms
``` ```
This will launch the server on port `8000`. To access it, visit `http://<YOUR_IP>:8000` (replace `<YOUR_IP>` with the IP address of the machine running Cup.) This will launch the server on port `8000`. To access it, visit `http://<YOUR_IP>:8000` (replace `<YOUR_IP>` with the IP address of the machine running Cup.)
@@ -29,13 +29,13 @@ Pass the `-p` argument with the port you want to use
```ansi ```ansi
$ cup serve -p 9000 $ cup serve -p 9000
INFO Starting server, please wait...  INFO Starting server, please wait...
INFO ✨ Checked 8 images in 8862ms  INFO ✨ Checked 8 images in 8862ms
INFO Ready to start!  INFO Ready to start!
HTTP GET / 200 in 0ms  HTTP GET / 200 in 0ms
HTTP GET /assets/index.js 200 in 3ms  HTTP GET /assets/index.js 200 in 3ms
HTTP GET /assets/index.css 200 in 0ms  HTTP GET /assets/index.css 200 in 0ms
HTTP GET /api/v3/json 200 in 0ms  HTTP GET /api/v3/json 200 in 0ms
``` ```
## Usage with Docker ## Usage with Docker

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -17,7 +17,7 @@ async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Ve
let handles: Vec<_> = ctx.config.servers let handles: Vec<_> = ctx.config.servers
.iter() .iter()
.map(|(name, url)| async { .map(|(name, url)| async move {
let base_url = if url.starts_with("http://") || url.starts_with("https://") { let base_url = if url.starts_with("http://") || url.starts_with("https://") {
format!("{}/api/v3/", url.trim_end_matches('/')) format!("{}/api/v3/", url.trim_end_matches('/'))
} else { } else {
@@ -47,6 +47,7 @@ async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Ve
return Vec::new(); return Vec::new();
} }
let json = parse_json(&get_response_body(response).await); let json = parse_json(&get_response_body(response).await);
ctx.logger.debug(format!("JSON response for {}: {}", name, json));
if let Some(updates) = json["images"].as_array() { if let Some(updates) = json["images"].as_array() {
let mut server_updates: Vec<Update> = updates let mut server_updates: Vec<Update> = updates
.iter() .iter()
@@ -57,6 +58,7 @@ async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Ve
update.server = Some(name.clone()); update.server = Some(name.clone());
update.status = update.get_status(); update.status = update.get_status();
} }
ctx.logger.debug(format!("Updates for {}: {:#?}", name, server_updates));
return server_updates; return server_updates;
} }
@@ -118,6 +120,16 @@ pub async fn get_updates(
.iter() .iter()
.map(|image| &image.parts.registry) .map(|image| &image.parts.registry)
.unique() .unique()
.filter(|&registry| match ctx.config.registries.get(registry) {
Some(config) => {
if config.ignore {
false
} else {
true
}
}
None => true,
})
.collect::<Vec<&String>>(); .collect::<Vec<&String>>();
// Create request client. All network requests share the same client for better performance. // Create request client. All network requests share the same client for better performance.
@@ -136,7 +148,7 @@ pub async fn get_updates(
// Retrieve an authentication token (if required) for each registry. // Retrieve an authentication token (if required) for each registry.
let mut tokens: FxHashMap<&str, Option<String>> = FxHashMap::default(); let mut tokens: FxHashMap<&str, Option<String>> = FxHashMap::default();
for registry in registries { for registry in registries.clone() {
let credentials = if let Some(registry_config) = ctx.config.registries.get(registry) { let credentials = if let Some(registry_config) = ctx.config.registries.get(registry) {
&registry_config.authentication &registry_config.authentication
} else { } else {
@@ -161,24 +173,11 @@ pub async fn get_updates(
ctx.logger.debug(format!("Tokens: {:?}", tokens)); ctx.logger.debug(format!("Tokens: {:?}", tokens));
let ignored_registries = ctx
.config
.registries
.iter()
.filter_map(|(registry, registry_config)| {
if registry_config.ignore {
Some(registry)
} else {
None
}
})
.collect::<Vec<&String>>();
let mut handles = Vec::with_capacity(images.len()); let mut handles = Vec::with_capacity(images.len());
// Loop through images check for updates // Loop through images check for updates
for image in &images { for image in &images {
let is_ignored = ignored_registries.contains(&&image.parts.registry) let is_ignored = !registries.contains(&&image.parts.registry)
|| ctx || ctx
.config .config
.images .images

View File

@@ -95,6 +95,10 @@ impl Client {
let message = format!("{} {}: Connection timed out!", method, url); let message = format!("{} {}: Connection timed out!", method, url);
self.ctx.logger.warn(&message); self.ctx.logger.warn(&message);
Err(message) Err(message)
} else if error.is_middleware() {
let message = format!("{} {}: Connection failed after 3 retries!", method, url);
self.ctx.logger.warn(&message);
Err(message)
} else { } else {
error!( error!(
"{} {}: Unexpected error: {}", "{} {}: Unexpected error: {}",

View File

@@ -35,6 +35,7 @@ pub struct VersionInfo {
pub struct Image { pub struct Image {
pub reference: String, pub reference: String,
pub parts: Parts, pub parts: Parts,
pub url: Option<String>,
pub digest_info: Option<DigestInfo>, pub digest_info: Option<DigestInfo>,
pub version_info: Option<VersionInfo>, pub version_info: Option<VersionInfo>,
pub error: Option<String>, pub error: Option<String>,
@@ -61,6 +62,7 @@ impl Image {
repository, repository,
tag, tag,
}, },
url: image.url(),
digest_info: Some(DigestInfo { digest_info: Some(DigestInfo {
local_digests, local_digests,
remote_digest: None, remote_digest: None,
@@ -141,6 +143,7 @@ impl Image {
Update { Update {
reference: self.reference.clone(), reference: self.reference.clone(),
parts: self.parts.clone(), parts: self.parts.clone(),
url: self.url.clone(),
result: UpdateResult { result: UpdateResult {
has_update: has_update.to_option_bool(), has_update: has_update.to_option_bool(),
info: match has_update { info: match has_update {

View File

@@ -3,6 +3,7 @@ use bollard::secret::{ImageInspect, ImageSummary};
pub trait InspectData { pub trait InspectData {
fn tags(&self) -> Option<&Vec<String>>; fn tags(&self) -> Option<&Vec<String>>;
fn digests(&self) -> Option<&Vec<String>>; fn digests(&self) -> Option<&Vec<String>>;
fn url(&self) -> Option<String>;
} }
impl InspectData for ImageInspect { impl InspectData for ImageInspect {
@@ -13,6 +14,16 @@ impl InspectData for ImageInspect {
fn digests(&self) -> Option<&Vec<String>> { fn digests(&self) -> Option<&Vec<String>> {
self.repo_digests.as_ref() self.repo_digests.as_ref()
} }
fn url(&self) -> Option<String> {
match &self.config {
Some(config) => match &config.labels {
Some(labels) => labels.get("org.opencontainers.image.url").cloned(),
None => None,
},
None => None,
}
}
} }
impl InspectData for ImageSummary { impl InspectData for ImageSummary {
@@ -23,4 +34,8 @@ impl InspectData for ImageSummary {
fn digests(&self) -> Option<&Vec<String>> { fn digests(&self) -> Option<&Vec<String>> {
Some(&self.repo_digests) Some(&self.repo_digests)
} }
fn url(&self) -> Option<String> {
self.labels.get("org.opencontainers.image.url").cloned()
}
} }

View File

@@ -1,8 +1,7 @@
use std::fmt::Display; use std::fmt::Display;
/// Enum for image status /// Enum for image status
#[derive(Ord, Eq, PartialEq, PartialOrd, Clone)] #[derive(Ord, Eq, PartialEq, PartialOrd, Clone, Debug)]
#[cfg_attr(test, derive(Debug))]
pub enum Status { pub enum Status {
UpdateMajor, UpdateMajor,
UpdateMinor, UpdateMinor,

View File

@@ -2,11 +2,12 @@ use serde::{ser::SerializeStruct, Deserialize, Serialize};
use super::{parts::Parts, status::Status}; use super::{parts::Parts, status::Status};
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Debug, Default))] #[cfg_attr(test, derive(PartialEq, Default))]
pub struct Update { pub struct Update {
pub reference: String, pub reference: String,
pub parts: Parts, pub parts: Parts,
pub url: Option<String>,
pub result: UpdateResult, pub result: UpdateResult,
pub time: u32, pub time: u32,
pub server: Option<String>, pub server: Option<String>,
@@ -14,16 +15,16 @@ pub struct Update {
pub status: Status, pub status: Status,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Debug, Default))] #[cfg_attr(test, derive(PartialEq, Default))]
pub struct UpdateResult { pub struct UpdateResult {
pub has_update: Option<bool>, pub has_update: Option<bool>,
pub info: UpdateInfo, pub info: UpdateInfo,
pub error: Option<String>, pub error: Option<String>,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Debug, Default))] #[cfg_attr(test, derive(PartialEq, Default))]
#[serde(untagged)] #[serde(untagged)]
pub enum UpdateInfo { pub enum UpdateInfo {
#[cfg_attr(test, default)] #[cfg_attr(test, default)]
@@ -32,8 +33,8 @@ pub enum UpdateInfo {
Digest(DigestUpdateInfo), Digest(DigestUpdateInfo),
} }
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Debug))] #[cfg_attr(test, derive(PartialEq))]
pub struct VersionUpdateInfo { pub struct VersionUpdateInfo {
pub version_update_type: String, pub version_update_type: String,
pub new_tag: String, pub new_tag: String,
@@ -41,8 +42,8 @@ pub struct VersionUpdateInfo {
pub new_version: String, pub new_version: String,
} }
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Debug))] #[cfg_attr(test, derive(PartialEq))]
pub struct DigestUpdateInfo { pub struct DigestUpdateInfo {
pub local_digests: Vec<String>, pub local_digests: Vec<String>,
pub remote_digest: Option<String>, pub remote_digest: Option<String>,
@@ -53,10 +54,12 @@ impl Serialize for VersionUpdateInfo {
where where
S: serde::Serializer, S: serde::Serializer,
{ {
let mut state = serializer.serialize_struct("VersionUpdateInfo", 3)?; let mut state = serializer.serialize_struct("VersionUpdateInfo", 5)?;
let _ = state.serialize_field("type", "version"); let _ = state.serialize_field("type", "version");
let _ = state.serialize_field("version_update_type", &self.version_update_type); let _ = state.serialize_field("version_update_type", &self.version_update_type);
let _ = state.serialize_field("new_tag", &self.new_tag); let _ = state.serialize_field("new_tag", &self.new_tag);
let _ = state.serialize_field("current_version", &self.current_version);
let _ = state.serialize_field("new_version", &self.new_version);
state.end() state.end()
} }
} }

View File

@@ -16,7 +16,7 @@ pub fn split(reference: &str) -> (String, String, String) {
} }
} }
}; };
let splits = repository_and_tag.split(':').collect::<Vec<&str>>(); let splits = repository_and_tag.split('@').next().unwrap().split(':').collect::<Vec<&str>>();
let (repository, tag) = match splits.len() { let (repository, tag) = match splits.len() {
1 | 2 => { 1 | 2 => {
let repository_components = splits[0].split('/').collect::<Vec<&str>>(); let repository_components = splits[0].split('/').collect::<Vec<&str>>();
@@ -38,7 +38,7 @@ pub fn split(reference: &str) -> (String, String, String) {
}; };
(repository, tag) (repository, tag)
} }
_ => unreachable!(), _ => {dbg!(splits); panic!()},
}; };
(registry.to_string(), repository, tag.to_string()) (registry.to_string(), repository, tag.to_string())
} }

View File

@@ -1 +0,0 @@
nodejs 22.8.0

Binary file not shown.

View File

@@ -225,182 +225,97 @@
</div> </div>
<ul> <ul>
{% for server in server_ids %} {% for server in server_ids %}
{% if server == '' %} <li class="mb-4 last:mb-0">
<li class="mb-8 last:mb-0"> <p
<ul class="my-4 text-lg font-semibold text-{{ theme }}-600 dark:text-{{ theme }}-400 px-6"
class="dark:divide-{{theme}}-900 divide-y dark:text-white" >
> {% if server == '' %}
{% for image in servers[server] %} Local images
<li {% else %}
class="flex items-center gap-4 break-all px-6 py-4 text-start hover:bg-{{theme}}-100 hover:dark:bg-{{theme}}-900/50 transition-colors duration-200"
>
<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="size-6 shrink-0 text-{{ theme }}-500"
>
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>
</svg>
<span class="font-mono">{{ image.name }}</span>
{% case image.status %}
{% when 'Up to
date' %}
<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="ml-auto text-green-500"
>
<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>
</svg>
{% when 'Unknown' %}
<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="text-{{ theme }}-500 ml-auto"
>
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>
</svg>
{% else %}
{% case image.status %}
{% when 'Major update' %}
{% assign color = 'text-red-500' %}
{% when 'Minor
update' %}
{% assign color = 'text-yellow-500' %}
{% else %}
{% assign color = 'text-blue-500' %}
{% endcase %}
<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="ml-auto {{ color }}"
>
<circle cx="12" cy="12" r="10"/><path d="m16 12-4-4-4 4"/><path d="M12 16V8"/>
</svg>
{% endcase %}
</li>
{% endfor %}
</ul>
</li>
{% else %}
<li class="mb-4 last:mb-0">
<p
class="my-4 text-lg font-semibold text-{{ theme }}-600 dark:text-{{ theme }}-400 px-6"
>
{{ server }} {{ server }}
</p> {% endif %}
<ul </p>
class="dark:divide-{{ theme }}-900 divide-y dark:text-white" <ul
> class="dark:divide-{{theme}}-900 divide-y dark:text-white"
{% for image in servers[server] %} >
<li {% for image in servers[server] %}
class="flex items-center gap-4 break-all px-6 py-4 text-start hover:bg-{{ theme }}-100 hover:dark:bg-{{ theme }}-900/50 transition-colors duration-200" <li
class="flex items-center gap-4 break-all px-6 py-4 text-start hover:bg-{{theme}}-100 hover:dark:bg-{{theme}}-900/50 transition-colors duration-200"
>
<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="size-6 shrink-0 text-{{ theme }}-500"
> >
<svg <path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>
xmlns="http://www.w3.org/2000/svg" </svg>
width="24" <span class="font-mono">{{ image.name }}</span>
height="24" {% case image.status %}
viewBox="0 0 24 24" {% when 'Up to date' %}
fill="none" <svg
stroke="currentColor" xmlns="http://www.w3.org/2000/svg"
stroke-width="2" width="24"
stroke-linecap="round" height="24"
stroke-linejoin="round" viewBox="0 0 24 24"
class="size-6 shrink-0" fill="none"
> stroke="currentColor"
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/> stroke-width="2"
</svg> stroke-linecap="round"
{{ image.name }} stroke-linejoin="round"
{% case image.status %} class="ml-auto text-green-500"
{% when 'Up to >
date' %} <circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>
<svg </svg>
xmlns="http://www.w3.org/2000/svg" {% when 'Unknown' %}
width="24" <svg
height="24" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" width="24"
fill="none" height="24"
stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2" fill="none"
stroke-linecap="round" stroke="currentColor"
stroke-linejoin="round" stroke-width="2"
class="ml-auto text-green-500" stroke-linecap="round"
> stroke-linejoin="round"
<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/> class="text-{{ theme }}-500 ml-auto"
</svg> >
{% when 'Unknown' %} <circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>
<svg </svg>
xmlns="http://www.w3.org/2000/svg" {% else %}
width="24" {% case image.status %}
height="24" {% when 'Major update' %}
viewBox="0 0 24 24" {% assign color = 'text-red-500' %}
fill="none" {% when 'Minor update' %}
stroke="currentColor" {% assign color = 'text-yellow-500' %}
stroke-width="2" {% else %}
stroke-linecap="round" {% assign color = 'text-blue-500' %}
stroke-linejoin="round" {% endcase %}
class="text-{{ theme }}-500 ml-auto" <svg
> xmlns="http://www.w3.org/2000/svg"
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/> width="24"
</svg> height="24"
{% else %} viewBox="0 0 24 24"
{% case image.status %} fill="none"
{% when 'Major update' %} stroke="currentColor"
{% assign color = 'text-red-500' %} stroke-width="2"
{% when 'Minor stroke-linecap="round"
update' %} stroke-linejoin="round"
{% assign color = 'text-yellow-500' %} class="ml-auto {{ color }}"
{% else %} >
{% assign color = 'text-blue-500' %} <circle cx="12" cy="12" r="10"/><path d="m16 12-4-4-4 4"/><path d="M12 16V8"/>
{% endcase %} </svg>
<svg {% endcase %}
xmlns="http://www.w3.org/2000/svg" </li>
width="24" {% endfor %}
height="24" </ul>
viewBox="0 0 24 24" </li>
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="ml-auto {{ color }}"
>
<circle cx="12" cy="12" r="10"/><path d="m16 12-4-4-4 4"/><path d="M12 16V8"/>
</svg>
{% endcase %}
</li>
{% endfor %}
</ul>
</li>
{% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View File

@@ -13,7 +13,6 @@
"dependencies": { "dependencies": {
"@headlessui/react": "^2.1.10", "@headlessui/react": "^2.1.10",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.2",
"caniuse-lite": "^1.0.30001698",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",

View File

@@ -21,6 +21,8 @@ export function CodeBlock({
}; };
}; };
const copyText = children instanceof Array ? children.join("") : children;
return ( return (
<div <div
className={`group relative flex w-full items-center rounded-lg bg-${theme}-100 px-3 py-2 font-mono text-${theme}-700 dark:bg-${theme}-950 dark:text-${theme}-300`} className={`group relative flex w-full items-center rounded-lg bg-${theme}-100 px-3 py-2 font-mono text-${theme}-700 dark:bg-${theme}-950 dark:text-${theme}-300`}
@@ -35,7 +37,7 @@ export function CodeBlock({
) : ( ) : (
<button <button
className={`duration-50 absolute right-3 bg-${theme}-100 py-1 pl-2 opacity-0 transition-opacity group-hover:opacity-100 dark:bg-${theme}-950`} className={`duration-50 absolute right-3 bg-${theme}-100 py-1 pl-2 opacity-0 transition-opacity group-hover:opacity-100 dark:bg-${theme}-950`}
onClick={handleCopy(`docker pull ${children}`)} onClick={handleCopy(`${copyText}`)}
> >
<Clipboard className="size-5" /> <Clipboard className="size-5" />
</button> </button>

View File

@@ -38,8 +38,11 @@ export default function Image({ data }: { data: Image }) {
data.result.info?.type == "version" data.result.info?.type == "version"
? data.reference.split(":")[0] + ":" + data.result.info.new_tag ? data.reference.split(":")[0] + ":" + data.result.info.new_tag
: data.reference; : data.reference;
const info = getInfo(data)!;
let url: string | null = null; let url: string | null = null;
if (clickable_registries.includes(data.parts.registry)) { if (data.url) {
url = data.url;
} else if (clickable_registries.includes(data.parts.registry)) {
switch (data.parts.registry) { switch (data.parts.registry) {
case "registry-1.docker.io": case "registry-1.docker.io":
url = `https://hub.docker.com/r/${data.parts.repository}`; url = `https://hub.docker.com/r/${data.parts.repository}`;
@@ -57,7 +60,12 @@ export default function Image({ data }: { data: Image }) {
> >
<Box className={`size-6 shrink-0 text-${theme}-500`} /> <Box className={`size-6 shrink-0 text-${theme}-500`} />
<span className="font-mono">{data.reference}</span> <span className="font-mono">{data.reference}</span>
<Icon data={data} /> <WithTooltip
text={info.description}
className={`ml-auto size-6 shrink-0 ${info.color}`}
>
<info.icon />
</WithTooltip>
</li> </li>
</button> </button>
<Dialog open={open} onClose={setOpen} className="relative z-10"> <Dialog open={open} onClose={setOpen} className="relative z-10">
@@ -76,14 +84,14 @@ export default function Image({ data }: { data: Image }) {
> >
<div className="mb-4 flex items-center gap-3"> <div className="mb-4 flex items-center gap-3">
<Box className={`size-6 shrink-0 text-${theme}-500`} /> <Box className={`size-6 shrink-0 text-${theme}-500`} />
<DialogTitle className="font-mono text-black dark:text-white"> <DialogTitle className="break-all font-mono text-black dark:text-white">
{url ? ( {url ? (
<> <>
<a <a
href={url} href={url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={`group w-fit text-black hover:underline dark:text-white`} className={`group w-fit hover:underline`}
> >
<span> <span>
{data.reference} {data.reference}
@@ -113,7 +121,8 @@ export default function Image({ data }: { data: Image }) {
</button> </button>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<DialogIcon data={data} /> <info.icon className={`size-6 shrink-0 ${info.color}`} />
{info.description}
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Timer className="size-6 shrink-0 text-gray-500" /> <Timer className="size-6 shrink-0 text-gray-500" />
@@ -164,118 +173,54 @@ export default function Image({ data }: { data: Image }) {
); );
} }
function Icon({ data }: { data: Image }) { function getInfo(data: Image):
| {
color: string;
icon: typeof HelpCircle;
description: string;
}
| undefined {
switch (data.result.has_update) { switch (data.result.has_update) {
case null: case null:
return ( return {
<WithTooltip color: "text-gray-500",
text="Unknown" icon: HelpCircle,
className="ml-auto size-6 shrink-0 text-gray-500" description: "Unknown",
> };
<HelpCircle />
</WithTooltip>
);
case false: case false:
return ( return {
<WithTooltip color: "text-green-500",
text="Up to date" icon: CircleCheck,
className="ml-auto size-6 shrink-0 text-green-500" description: "Up to date",
> };
<CircleCheck />
</WithTooltip>
);
case true: case true:
if (data.result.info?.type === "version") { if (data.result.info?.type === "version") {
switch (data.result.info.version_update_type) { switch (data.result.info.version_update_type) {
case "major": case "major":
return ( return {
<WithTooltip color: "text-red-500",
text="Major Update" icon: CircleArrowUp,
className="ml-auto size-6 shrink-0 text-red-500" description: "Major update",
> };
<CircleArrowUp />
</WithTooltip>
);
case "minor": case "minor":
return ( return {
<WithTooltip color: "text-yellow-500",
text="Minor Update" icon: CircleArrowUp,
className="ml-auto size-6 shrink-0 text-yellow-500" description: "Minor update",
> };
<CircleArrowUp />
</WithTooltip>
);
case "patch": case "patch":
return ( return {
<WithTooltip color: "text-blue-500",
text="Patch Update" icon: CircleArrowUp,
className="ml-auto size-6 shrink-0 text-blue-500" description: "Patch update",
> };
<CircleArrowUp />
</WithTooltip>
);
} }
} else if (data.result.info?.type === "digest") { } else if (data.result.info?.type === "digest") {
return ( return {
<WithTooltip color: "text-blue-500",
text="Update available" icon: CircleArrowUp,
className="ml-auto size-6 shrink-0 text-blue-500" description: "Update available",
> };
<CircleArrowUp />
</WithTooltip>
);
}
}
}
function DialogIcon({ data }: { data: Image }) {
switch (data.result.has_update) {
case null:
return (
<>
<HelpCircle className="size-6 shrink-0 text-gray-500" />
Unknown
</>
);
case false:
return (
<>
<CircleCheck className="size-6 shrink-0 text-green-500" />
Up to date
</>
);
case true:
if (data.result.info?.type === "version") {
switch (data.result.info.version_update_type) {
case "major":
return (
<>
<CircleArrowUp className="size-6 shrink-0 text-red-500" />
Major update
</>
);
case "minor":
return (
<>
<CircleArrowUp className="size-6 shrink-0 text-yellow-500" />
Minor update
</>
);
case "patch":
return (
<>
<CircleArrowUp className="size-6 shrink-0 text-blue-500" />
Patch update
</>
);
}
} else if (data.result.info?.type === "digest") {
return (
<>
<CircleArrowUp className="size-6 shrink-0 text-blue-500" />
Update available
</>
);
} }
} }
} }

View File

@@ -1,7 +1,7 @@
import { Data } from "../types"; import { Data } from "../types";
import Logo from "./Logo"; import Logo from "./Logo";
import { theme } from "../theme"; import { theme } from "../theme";
import { RefreshCw } from "lucide-react"; import { LoaderCircle } from "lucide-react";
export default function Loading({ onLoad }: { onLoad: (data: Data) => void }) { export default function Loading({ onLoad }: { onLoad: (data: Data) => void }) {
fetch( fetch(
@@ -26,9 +26,16 @@ export default function Loading({ onLoad }: { onLoad: (data: Data) => void }) {
<Logo /> <Logo />
</div> </div>
<div <div
className={`flex h-full items-center justify-center gap-1 text-${theme}-500 dark:text-${theme}-400`} className={`flex flex-col h-full items-center justify-center gap-1 text-${theme}-500 dark:text-${theme}-400`}
> >
Loading <RefreshCw className="animate-spin" /> <div className="flex gap-1 mb-8">
Loading <LoaderCircle className="animate-spin" />
</div>
<p>
If this takes more than a few seconds, there was probably a
problem fetching the data. Please try reloading the page and
report a bug if the problem persists.
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -21,7 +21,7 @@ export default function RefreshButton() {
}; };
return ( return (
<WithTooltip text="Reload"> <WithTooltip text="Reload">
<button className="group" onClick={refresh} disabled={disabled}> <button className="group shrink-0" onClick={refresh} disabled={disabled}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
@@ -32,7 +32,7 @@ export default function RefreshButton() {
strokeWidth="2" strokeWidth="2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
className="shrink-0 group-disabled:animate-spin" className="size-6 group-disabled:animate-spin"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <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="M4,11A8.1,8.1 0 0 1 19.5,9M20,5v4h-4" />

View File

@@ -20,6 +20,7 @@ export interface Image {
repository: string; repository: string;
tag: string; tag: string;
}; };
url: string | null;
result: { result: {
has_update: boolean | null; has_update: boolean | null;
info: VersionInfo | DigestInfo | null; info: VersionInfo | DigestInfo | null;