Compare commits
95 Commits
v3.4.2
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d0da37e36 | ||
|
|
780d7a088d | ||
|
|
bcb9f63735 | ||
|
|
4d691dd5fa | ||
|
|
685219ea62 | ||
|
|
756462cd7c | ||
|
|
f020ac0906 | ||
|
|
4b03a48d88 | ||
|
|
ba1cfac64b | ||
|
|
05d4c7c630 | ||
|
|
cf22ec300f | ||
|
|
5b428dbf67 | ||
|
|
787a730ab5 | ||
|
|
925989fd80 | ||
|
|
5656003058 | ||
|
|
f79d7ff03a | ||
|
|
550fb955a3 | ||
|
|
6ae95bf83b | ||
|
|
2262df0355 | ||
|
|
1beb7dc020 | ||
|
|
a0de565367 | ||
|
|
0314ef2f05 | ||
|
|
f1c8a45122 | ||
|
|
ce3f8176f1 | ||
|
|
8b520182ed | ||
|
|
e8fee79d20 | ||
|
|
24f160803a | ||
|
|
2ef77c9a55 | ||
|
|
a5bbdd0e33 | ||
|
|
b5aa0309ee | ||
|
|
4bbb53cd67 | ||
|
|
3ac6fb57e9 | ||
|
|
ead74dadd6 | ||
|
|
6e6afdb757 | ||
|
|
0c10134829 | ||
|
|
c0c7f7c0e9 | ||
|
|
aeeffaccba | ||
|
|
a1711b7ac8 | ||
|
|
9d628e3ab2 | ||
|
|
d3b18a6587 | ||
|
|
76a812f52f | ||
|
|
fe779c9c4e | ||
|
|
84609d5189 | ||
|
|
ded441cf75 | ||
|
|
0a8295fff4 | ||
|
|
9c8e6ccdea | ||
|
|
f1e1bcbf1c | ||
|
|
31f7bfbbcb | ||
|
|
15eb553e50 | ||
|
|
359147770f | ||
|
|
0a4e302322 | ||
|
|
5ed64c92fd | ||
|
|
6d08d75ac3 | ||
|
|
dc38b84e87 | ||
|
|
09b6880295 | ||
|
|
4f1075b2b2 | ||
|
|
c84270603f | ||
|
|
4aa28f2cc5 | ||
|
|
eadda5f776 | ||
|
|
622b156eed | ||
|
|
dca19b5ae2 | ||
|
|
f6ac43aac0 | ||
|
|
e5e60c4abc | ||
|
|
33a72c8c0d | ||
|
|
e544ef6ca5 | ||
|
|
afc34a0847 | ||
|
|
ce08e00bb4 | ||
|
|
6a77b85141 | ||
|
|
215e88ae0f | ||
|
|
178acfb2f6 | ||
|
|
59894343de | ||
|
|
61bc60493f | ||
|
|
be7d55d126 | ||
|
|
36a3a13c04 | ||
|
|
d85fadfb39 | ||
|
|
0f95be26dc | ||
|
|
0b7e064980 | ||
|
|
9e9bb78db7 | ||
|
|
88d346b480 | ||
|
|
4519c534a1 | ||
|
|
6b83f51749 | ||
|
|
0c3f293fa8 | ||
|
|
d94abecf35 | ||
|
|
c11b5e6432 | ||
|
|
022dc0b2cb | ||
|
|
51609da4ff | ||
|
|
3ed79e69bd | ||
|
|
078a51c4fa | ||
|
|
8d70d7ae4d | ||
|
|
6d45409928 | ||
|
|
bcfb9ef27a | ||
|
|
5c4de36052 | ||
|
|
eda30229e2 | ||
|
|
8fd012efbe | ||
|
|
8ab073d562 |
13
.github/actions/build-image/Dockerfile
vendored
@@ -1,13 +0,0 @@
|
||||
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
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT ["/cup"]
|
||||
52
.github/actions/build-image/action.yml
vendored
@@ -1,52 +0,0 @@
|
||||
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
|
||||
7
.github/workflows/ci.yml
vendored
@@ -14,8 +14,13 @@ jobs:
|
||||
- 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@v2
|
||||
uses: oven-sh/setup-bun@v1
|
||||
|
||||
- name: Install deps
|
||||
run: cd web && bun install
|
||||
|
||||
7
.github/workflows/docs.yml
vendored
@@ -2,7 +2,7 @@ name: Deploy github pages
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- "docs/**"
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
build:
|
||||
@@ -15,8 +15,11 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
uses: oven-sh/setup-bun@v1
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
- name: Build
|
||||
|
||||
42
.github/workflows/nightly.yml
vendored
@@ -32,8 +32,13 @@ jobs:
|
||||
- name: Install cross
|
||||
run: RUSTFLAGS="" cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
uses: oven-sh/setup-bun@v1
|
||||
|
||||
- name: Install deps
|
||||
run: cd web && bun install
|
||||
@@ -57,25 +62,38 @@ jobs:
|
||||
cup-linux-arm64
|
||||
|
||||
build-image:
|
||||
needs:
|
||||
- get-tag
|
||||
- build-binaries
|
||||
needs: get-tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
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:
|
||||
tags: |
|
||||
${{ needs.get-tag.outputs.tag }}
|
||||
gh-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ 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:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- get-tag
|
||||
- build-binaries
|
||||
- build-image
|
||||
needs: [build-binaries, get-tag]
|
||||
steps:
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
40
.github/workflows/release.yml
vendored
@@ -30,8 +30,13 @@ jobs:
|
||||
- name: Install cross
|
||||
run: RUSTFLAGS="" cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
uses: oven-sh/setup-bun@v1
|
||||
|
||||
- name: Install deps
|
||||
run: cd web && bun install
|
||||
@@ -55,19 +60,34 @@ jobs:
|
||||
cup-linux-arm64
|
||||
|
||||
build-image:
|
||||
needs:
|
||||
- get-tag
|
||||
- build-binaries
|
||||
needs: get-tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
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:
|
||||
tags: |
|
||||
${{ needs.get-tag.outputs.tag }}
|
||||
latest
|
||||
gh-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ 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:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -87,4 +107,4 @@ jobs:
|
||||
prerelease: true
|
||||
tag_name: ${{ needs.get-tag.outputs.tag }}
|
||||
name: ${{ needs.get-tag.outputs.tag }}
|
||||
files: binaries/*
|
||||
files: binaries/*
|
||||
@@ -7,7 +7,7 @@ First of all, thanks for taking time to contribute to Cup! This guide will help
|
||||
Requirements:
|
||||
- A computer running Linux
|
||||
- Rust (usually installed from https://rustup.rs/)
|
||||
- Bun 1+
|
||||
- Node.js 22+ and Bun 1+
|
||||
|
||||
1. Fork the repository. This is where you'll be pushing your changes before you create a pull request. Make sure to _create a new branch_ for your changes.
|
||||
2. Clone your fork with `git clone https://github.com/<YOUR_USERNAME>/cup` (if you use SSH, `git clone git@github.com:<YOUR_USERNAME>/cup`) and open your editor
|
||||
@@ -48,4 +48,4 @@ After you're done with that, commit your changes and push them to your branch.
|
||||
|
||||
Next, open your fork on Github and create a pull request. Make sure to include the changes you made, which issues it addresses (if any) and any other info you think is important.
|
||||
|
||||
Happy contributing!
|
||||
Happy contributing!
|
||||
1220
Cargo.lock
generated
12
Cargo.toml
@@ -1,15 +1,15 @@
|
||||
[package]
|
||||
name = "cup"
|
||||
version = "3.4.2"
|
||||
version = "3.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.7", features = ["derive"] }
|
||||
indicatif = { version = "0.17.8", optional = true }
|
||||
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
|
||||
xitca-web = { version = "0.6.2", optional = true }
|
||||
xitca-web = { version = "0.5.0", optional = true }
|
||||
liquid = { version = "0.26.6", optional = true }
|
||||
bollard = "0.18.1"
|
||||
bollard = "0.16.1"
|
||||
once_cell = "1.19.0"
|
||||
http-auth = { version = "0.1.9", default-features = false }
|
||||
termsize = { version = "0.1.8", optional = true }
|
||||
@@ -17,16 +17,14 @@ regex = { version = "1.10.5", default-features = false, features = ["perf"] }
|
||||
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"] }
|
||||
futures = "0.3.30"
|
||||
reqwest-retry = "0.7.0"
|
||||
reqwest-retry = "0.6.1"
|
||||
reqwest-middleware = "0.3.3"
|
||||
rustc-hash = "2.0.0"
|
||||
http-link = "1.0.1"
|
||||
itertools = "0.14.0"
|
||||
itertools = "0.13.0"
|
||||
serde_json = "1.0.133"
|
||||
serde = "1.0.215"
|
||||
tokio-cron-scheduler = { version = "0.13.0", default-features = false, optional = true }
|
||||
envy = "0.4.2"
|
||||
chrono-tz = "0.10.3"
|
||||
|
||||
[features]
|
||||
default = ["server", "cli"]
|
||||
|
||||
23
Dockerfile
@@ -1,21 +1,21 @@
|
||||
### Build UI ###
|
||||
FROM oven/bun:1-alpine AS web
|
||||
FROM node:20 AS web
|
||||
|
||||
# Copy package.json and lockfile from web
|
||||
WORKDIR /web
|
||||
COPY ./web/package.json ./web/bun.lock ./
|
||||
|
||||
# Install requirements
|
||||
RUN bun install
|
||||
# Install bun
|
||||
RUN curl -fsSL https://bun.sh/install | bash
|
||||
|
||||
# Copy web folder
|
||||
COPY ./web .
|
||||
COPY ./web /web
|
||||
WORKDIR /web
|
||||
|
||||
# Install requirements
|
||||
RUN ~/.bun/bin/bun install
|
||||
|
||||
# Build frontend
|
||||
RUN bun run build
|
||||
RUN ~/.bun/bin/bun run build
|
||||
|
||||
### Build Cup ###
|
||||
FROM rust:1-alpine AS build
|
||||
FROM rust:1.80.1-alpine AS build
|
||||
|
||||
# Requirements
|
||||
RUN apk add musl-dev
|
||||
@@ -39,5 +39,4 @@ FROM scratch
|
||||
# Copy binary
|
||||
COPY --from=build /cup/target/release/cup /cup
|
||||
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT ["/cup"]
|
||||
ENTRYPOINT ["/cup"]
|
||||
20
README.md
@@ -1,13 +1,5 @@
|
||||
# Cup 🥤
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
[](https://discord.gg/jmh5ctzwNG)
|
||||
|
||||
|
||||
Cup is the easiest way to check for container image updates.
|
||||
|
||||

|
||||
@@ -23,20 +15,20 @@ _If you like this project and/or use Cup, please consider starring the project
|
||||
|
||||
- Extremely fast. Cup takes full advantage of your CPU and is hightly optimized, resulting in lightning fast speed. On my Raspberry Pi 5, it took 3.7 seconds for 58 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. I feel that this feature is especially relevant now with [Docker Hub reducing its pull limits for unauthenticated users](https://docs.docker.com/docker-hub/usage/).
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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 📘
|
||||
|
||||
Take a look at https://cup.sergi0g.dev/docs!
|
||||
Take a look at https://sergi0g.github.io/cup/docs!
|
||||
|
||||
## 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 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).
|
||||
- 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).
|
||||
|
||||
## Roadmap
|
||||
Take a sneak peek at what's coming up in future releases on the [roadmap](https://github.com/users/sergi0g/projects/2)!
|
||||
@@ -52,11 +44,11 @@ Here are some ideas to get you started:
|
||||
- Help optimize Cup and make it even better!
|
||||
- Add more features to the web UI
|
||||
|
||||
For more information, check the [docs](https://cup.sergi0g.dev/docs/contributing)!
|
||||
For more information, check the [docs](https://sergi0g.github.io/cup/docs/contributing)!
|
||||
|
||||
## Support
|
||||
|
||||
If you have any questions about Cup, feel free to ask in the [discussions](https://github.com/sergi0g/cup/discussions)! You can also join our [discord server](https://discord.gg/jmh5ctzwNG).
|
||||
If you have any questions about Cup, feel free to ask in the [discussions](https://github.com/sergi0g/cup/discussions)!
|
||||
|
||||
If you find a bug, or want to propose a feature, search for it in the [issues](https://github.com/sergi0g/cup/issues). If there isn't already an open issue, please open one.
|
||||
|
||||
|
||||
@@ -14,16 +14,6 @@
|
||||
"type": "boolean",
|
||||
"description": "Whether or not to enable agent mode. When agent mode is enabled, the server only exposes the API and the web interface is unavailable."
|
||||
},
|
||||
"ignore_update_type": {
|
||||
"type": "string",
|
||||
"description": "The types of updates to ignore. Ignoring an update type also implies ignoring all update types less specific than it. For example, ignoring patch updates also implies ignoring major and minor updates.",
|
||||
"enum": [
|
||||
"none",
|
||||
"major",
|
||||
"minor",
|
||||
"patch"
|
||||
]
|
||||
},
|
||||
"images": {
|
||||
"type": "object",
|
||||
"description": "Configuration options for specific images",
|
||||
@@ -46,7 +36,7 @@
|
||||
},
|
||||
"refresh_interval": {
|
||||
"type": "string",
|
||||
"description": "The interval at which Cup should check for updates. Must be a valid cron expression. Seconds are not optional. Reference: https://github.com/Hexagon/croner-rust#pattern",
|
||||
"description": "The interval at which Cup should check for updates. Must be a valid cron expression. Reference: https://github.com/Hexagon/croner-rust#pattern",
|
||||
"minLength": 11
|
||||
},
|
||||
"registries": {
|
||||
@@ -69,8 +59,8 @@
|
||||
}
|
||||
},
|
||||
"socket": {
|
||||
"type": "string",
|
||||
"description": "The path to the unix socket you would like Cup to use for communication with the Docker daemon. Useful if you're trying to use Cup with Podman.",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"servers": {
|
||||
@@ -83,8 +73,8 @@
|
||||
"minProperties": 1
|
||||
},
|
||||
"theme": {
|
||||
"type": "string",
|
||||
"description": "The theme used by the web UI",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"blue"
|
||||
|
||||
1661
docs/bun.lock
BIN
docs/bun.lockb
Executable file
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build && pagefind --site out --output-path out/_pagefind",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"fmt": "bun prettier --write ."
|
||||
@@ -12,7 +12,7 @@
|
||||
"dependencies": {
|
||||
"@tabler/icons-react": "^3.29.0",
|
||||
"geist": "^1.3.1",
|
||||
"next": "15.2.4",
|
||||
"next": "15.1.5",
|
||||
"nextra": "^4.1.0",
|
||||
"nextra-theme-docs": "^4.1.0",
|
||||
"react": "^19.0.0",
|
||||
@@ -21,16 +21,16 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@tailwindcss/postcss": "^4.0.1",
|
||||
"@types/bun": "^1.2.10",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-next": "15.1.5",
|
||||
"pagefind": "^1.3.0",
|
||||
"postcss": "^8.5.1",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^4.0.1",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 136 KiB |
15
docs/src/app/assets/GitHubIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 233 KiB |
@@ -10,12 +10,12 @@ export function Card({
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="p-4 bg-white dark:bg-black group">
|
||||
<Icon className="text-black size-7 group-hover:size-9 dark:text-white inline mr-2 transition-[width,height] duration-200" />
|
||||
<div>
|
||||
<Icon className="text-black size-7 dark:text-white inline mr-2" />
|
||||
<span className="align-middle text-2xl font-bold text-black dark:text-white">
|
||||
{name}
|
||||
</span>
|
||||
<p className="text-xl font-semibold text-neutral-500 dark:text-neutral-500">
|
||||
<p className="text-2xl font-semibold text-neutral-500 dark:text-neutral-500">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
28
docs/src/app/components/CopyableCode.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export function GridPattern() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
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"
|
||||
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"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
@@ -22,6 +22,7 @@ export function GridPattern() {
|
||||
<path
|
||||
d={`M.5 ${SIZE}V.5H${SIZE}`}
|
||||
fill="none"
|
||||
strokeDasharray={"4 2"}
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
28
docs/src/app/components/Section.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +1,29 @@
|
||||
import React from "react";
|
||||
import "./styles.css";
|
||||
import "./styles.css"
|
||||
|
||||
import CopyableCode from "../CopyableCode";
|
||||
import { Browser } from "../Browser";
|
||||
import { Card } from "../Card";
|
||||
import {
|
||||
IconAdjustments,
|
||||
IconArrowRight,
|
||||
IconBarrierBlockOff,
|
||||
IconBolt,
|
||||
IconBraces,
|
||||
IconDevices,
|
||||
IconFeather,
|
||||
IconGitMerge,
|
||||
IconPuzzle,
|
||||
IconServer,
|
||||
IconTerminal,
|
||||
IconLockCheck,
|
||||
} from "@tabler/icons-react";
|
||||
import { GitHubIcon } from "nextra/icons";
|
||||
import { GridPattern } from "../GridPattern";
|
||||
import { GradientText } from "../GradientText";
|
||||
import { Section } from "../Section";
|
||||
import { Steps } from "nextra/components";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<>
|
||||
<div className="relative home bg-radial-[ellipse_at_center] from-transparent from-20% to-white dark:to-black">
|
||||
<div className="relative home">
|
||||
<GridPattern />
|
||||
<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">
|
||||
@@ -36,7 +37,7 @@ export default async function Home() {
|
||||
blur={30}
|
||||
/>
|
||||
</h1>
|
||||
<h3 className="mx-auto mt-6 max-w-3xl text-center text-xl leading-tight font-medium text-neutral-500 dark:text-neutral-400">
|
||||
<h3 className="mx-auto mt-6 max-w-3xl text-center text-xl leading-tight font-medium text-gray-400">
|
||||
Cup is a small utility with a big impact. Simplify your
|
||||
container management workflow with fast and efficient update
|
||||
checking, a full-featured CLI and web interface, and more.
|
||||
@@ -53,7 +54,7 @@ export default async function Home() {
|
||||
<a
|
||||
href="https://github.com/sergi0g/cup"
|
||||
target="_blank"
|
||||
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"
|
||||
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"
|
||||
>
|
||||
Star on GitHub
|
||||
<GitHubIcon className="ml-auto size-4 md:size-5" />
|
||||
@@ -65,49 +66,68 @@ export default async function Home() {
|
||||
<Browser />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-black py-12 px-8 w-full">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<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
|
||||
name="Built for speed."
|
||||
icon={IconBolt}
|
||||
description="Cup is written in Rust and every release goes through extensive profiling to squeeze out every last drop of performance."
|
||||
/>
|
||||
<Card
|
||||
name="Configurable."
|
||||
icon={IconAdjustments}
|
||||
description="Make Cup yours with the extensive configuration options available. Customize and tailor it to your needs."
|
||||
/>
|
||||
<Card
|
||||
name="Extend it."
|
||||
icon={IconPuzzle}
|
||||
description="JSON output enables you to connect Cup with your favorite integrations, build automations and more."
|
||||
/>
|
||||
<Card
|
||||
name="CLI available."
|
||||
icon={IconTerminal}
|
||||
description="Do you like terminals? Cup has a CLI. Check for updates quickly without spinning up a server."
|
||||
/>
|
||||
<Card
|
||||
name="Multiple servers."
|
||||
icon={IconServer}
|
||||
description="Run multiple Cup instances and effortlessly check on them through one web interface."
|
||||
/>
|
||||
<Card
|
||||
name="Unstoppable."
|
||||
icon={IconBarrierBlockOff}
|
||||
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."
|
||||
/>
|
||||
<Card
|
||||
name="Lightweight."
|
||||
icon={IconFeather}
|
||||
description="No need for a powerful server and endless storage. The tiny 5.4 MB binary won't hog your CPU and memory."
|
||||
/>
|
||||
<Card
|
||||
name="Open source."
|
||||
icon={IconGitMerge}
|
||||
description="All source code is publicly available in our GitHub repository. We're looking for contributors!"
|
||||
/>
|
||||
<Section
|
||||
title="Powerful at its core."
|
||||
className="bg-gradient-to-r from-red-500 to-amber-500"
|
||||
>
|
||||
<Card
|
||||
name="100% Safe Code"
|
||||
icon={IconLockCheck}
|
||||
description="Built with safe Rust and Typescript to ensure security and reliability."
|
||||
/>
|
||||
<Card
|
||||
name="Lightning Fast Performance"
|
||||
icon={IconBolt}
|
||||
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
|
||||
name="Lightweight"
|
||||
icon={IconFeather}
|
||||
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."
|
||||
/>
|
||||
</Section>
|
||||
<Section
|
||||
title="Efficient, yet flexible."
|
||||
className="bg-gradient-to-r from-blue-500 to-indigo-500"
|
||||
>
|
||||
<Card
|
||||
name="JSON output"
|
||||
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!"
|
||||
icon={IconBraces}
|
||||
/>
|
||||
<Card
|
||||
name="Both CLI and web interface"
|
||||
description="Whether you prefer the command line or the web, Cup runs wherever you choose."
|
||||
icon={IconDevices}
|
||||
/>
|
||||
<Card
|
||||
name="Configurable"
|
||||
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."
|
||||
icon={IconAdjustments}
|
||||
/>
|
||||
</Section>
|
||||
<div className="relative py-24 border-t border-t-neutral-300 dark:border-t-neutral-600/30 text-black dark:text-white">
|
||||
<GridPattern />
|
||||
<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>
|
||||
|
||||
@@ -20,7 +20,7 @@ const logo = (
|
||||
);
|
||||
|
||||
const navbar = (
|
||||
<Navbar logo={logo} projectLink="https://github.com/sergi0g/cup" chatLink="https://discord.gg/jmh5ctzwNG">
|
||||
<Navbar logo={logo} projectLink="https://github.com/sergi0g/cup">
|
||||
<ThemeSwitch lite className="cursor-pointer" />
|
||||
</Navbar>
|
||||
);
|
||||
@@ -45,7 +45,7 @@ export default async function RootLayout({
|
||||
navbar={navbar}
|
||||
pageMap={await getPageMap()}
|
||||
footer={footer}
|
||||
docsRepositoryBase="https://github.com/sergi0g/cup/blob/main/docs"
|
||||
docsRepositoryBase="https://github.com/sergi0g/cup"
|
||||
>
|
||||
<div>{children}</div>
|
||||
</Layout>
|
||||
|
||||
@@ -10,6 +10,7 @@ const toc: Heading[] = [];
|
||||
export const metadata: NextraMetadata = {
|
||||
title: "Cup - The easiest way to manage your container updates",
|
||||
description: "Simple, fast, efficient Docker image update checking",
|
||||
filePath: "",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Callout } from "nextra/components";
|
||||
|
||||
# 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 use makes it ideal for this use case.
|
||||
@@ -37,13 +35,9 @@ services:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
```
|
||||
|
||||
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:
|
||||
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 docker compose:
|
||||
```yaml
|
||||
user: "1000:999"
|
||||
```
|
||||
|
||||
<Callout>
|
||||
You can use the command `getent group docker | cut -d: -f3` to find the group id for the docker group.
|
||||
</Callout>
|
||||
|
||||
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!
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
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
|
||||
|
||||
[](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:
|
||||
[](https://www.buymeacoffee.com/bastgau)
|
||||
@@ -36,7 +36,7 @@ services:
|
||||
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: updates_available
|
||||
homepage.widget.mappings[2].field.metrics: update_available
|
||||
homepage.widget.mappings[2].format: number
|
||||
```
|
||||
|
||||
@@ -64,7 +64,7 @@ widget:
|
||||
label: Up to date
|
||||
format: number
|
||||
- field:
|
||||
metrics: updates_available
|
||||
metrics: update_available
|
||||
label: Available updates
|
||||
format: number
|
||||
- field:
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# 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
|
||||
}
|
||||
```
|
||||
@@ -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:
|
||||
|
||||
```jsonc
|
||||
```json
|
||||
{
|
||||
"registries": {
|
||||
"<YOUR_REGISTRY_DOMAIN_1>": {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Callout } from "nextra/components";
|
||||
|
||||
# Automatic refresh
|
||||
|
||||
Cup can automatically refresh the results when running in server mode. Simply add this to your config:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"refresh_interval": "0 */30 * * * *", // Check twice an hour
|
||||
// Other options
|
||||
}
|
||||
```
|
||||
|
||||
You can use a cron expression to specify the refresh interval. Note that seconds are not optional. The reference is [here](https://github.com/Hexagon/croner-rust#pattern).
|
||||
|
||||
<Callout>
|
||||
If you use a schedule with absolute time (e.g. every day at 6 AM), make sure to set the `TZ` environment variable to your [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List).
|
||||
</Callout>
|
||||
@@ -1,22 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Callout } from "nextra/components";
|
||||
|
||||
# Ignored update types
|
||||
|
||||
To ignore certain update types, you can modify your config like this:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"ignore_update_type": "minor"
|
||||
}
|
||||
```
|
||||
|
||||
Available options are:
|
||||
|
||||
- `none`: Do not ignore any update types (default).
|
||||
- `major`: Ignore major updates.
|
||||
- `minor`: Ignore major and minor updates.
|
||||
- `patch`: Ignore major, minor and patch updates.
|
||||
|
||||
<Callout emoji="⚠️">
|
||||
Ignoring an update type also implies ignoring all update types less specific than it.
|
||||
For example, ignoring patch updates also implies ignoring major and minor updates.
|
||||
</Callout>
|
||||
@@ -1,35 +0,0 @@
|
||||
# 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!
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
IconLockOpen,
|
||||
IconKey,
|
||||
IconPlug,
|
||||
IconServer,
|
||||
IconServer
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
# Configuration
|
||||
@@ -71,21 +71,12 @@ Here's a full example:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/sergi0g/cup/main/cup.schema.json",
|
||||
"version": 3,
|
||||
"images": {
|
||||
"exclude": ["ghcr.io/immich-app/immich-machine-learning"],
|
||||
"extra": ["ghcr.io/sergi0g/cup:v3.0.0"]
|
||||
"authentication": {
|
||||
"ghcr.io": "<YOUR_TOKEN_HERE>",
|
||||
"registry-1.docker.io": "<YOUR_TOKEN_HERE>"
|
||||
},
|
||||
"registries": {
|
||||
"myregistry.com": {
|
||||
"authentication": "<YOUR_TOKEN_HERE>"
|
||||
}
|
||||
},
|
||||
"servers": {
|
||||
"Raspberry Pi": "https://server.local:8000"
|
||||
},
|
||||
"theme": "blue"
|
||||
"theme": "blue",
|
||||
"insecure_registries": ["localhost:5000", "my-insecure-registry.example.com"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -109,36 +100,3 @@ $ docker run -tv /var/run/docker.sock:/var/run/docker.sock -v /home/sergio/.conf
|
||||
```
|
||||
|
||||
</Steps>
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Want to make a quick change without editing your `config.json`? Cup also supports some configuration options from environment variables.
|
||||
Here are the ones currently available:
|
||||
- `CUP_AGENT` - Agent mode
|
||||
- `CUP_IGNORE_UPDATE_TYPE` - Ignoring specific update types
|
||||
- `CUP_REFRESH_INTERVAL` - Automatic refresh
|
||||
- `CUP_SOCKET` - Socket
|
||||
- `CUP_THEME` - Theme
|
||||
|
||||
Refer to the configuration page for more information on each of these.
|
||||
|
||||
Here's an example of a Docker Compose file using them:
|
||||
```yaml
|
||||
services:
|
||||
cup:
|
||||
image: ghcr.io/sergi0g/cup:latest
|
||||
command: serve
|
||||
ports:
|
||||
- 8000:8000
|
||||
environment:
|
||||
- CUP_AGENT: true
|
||||
- CUP_IGNORE_UPDATE_TYPE: major
|
||||
- CUP_REFRESH_INTERVAL: "0 */30 * * * *"
|
||||
- CUP_SOCKET: tcp://localhost:2375
|
||||
- CUP_THEME: blue
|
||||
```
|
||||
|
||||
<Callout>
|
||||
Heads up!
|
||||
Any configuration option you set with environment variables **always** overrides anything in your `cup.json`.
|
||||
</Callout>
|
||||
@@ -8,7 +8,7 @@ To solve this problem, you can specify exceptions in your `cup.json`.
|
||||
|
||||
Here's what it looks like:
|
||||
|
||||
```jsonc
|
||||
```json
|
||||
{
|
||||
"registries": {
|
||||
"<INSECURE_REGISTRY_1>": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
```jsonc
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"Cool server 1": "http://your-other-server-running-cup:8000",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Custom socket
|
||||
# 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:
|
||||
|
||||
```jsonc
|
||||
```json
|
||||
{
|
||||
"socket": "/run/user/1000/podman/podman.sock"
|
||||
// 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):
|
||||
|
||||
```jsonc
|
||||
```json
|
||||
{
|
||||
"socket": "tcp://localhost:2375"
|
||||
// Other options
|
||||
|
||||
@@ -21,7 +21,7 @@ Available options are `default` and `blue`.
|
||||
|
||||
Here's an example:
|
||||
|
||||
```jsonc
|
||||
```json
|
||||
{
|
||||
"theme": "blue"
|
||||
// Other options
|
||||
|
||||
@@ -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)
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Callout, Cards } from "nextra/components";
|
||||
import { IconServer, IconTerminal } from "@tabler/icons-react";
|
||||
import { IconServer, IconTerminal } from "@tabler/icons-react"
|
||||
|
||||
# Integrations
|
||||
|
||||
@@ -34,7 +34,6 @@ The data returned from the API or from the CLI is in JSON and looks like this:
|
||||
"repository": "sergi0g/cup",
|
||||
"tag": "latest",
|
||||
},
|
||||
"url": "https://github.com/sergi0g/cup", // The URL specified in the "org.opencontainers.image.url" label, otherwise null
|
||||
"result": {
|
||||
"has_update": true, // `true` when an image has an update of any kind, `false` when up to date and `null` when unknown.
|
||||
"info": {
|
||||
@@ -78,7 +77,3 @@ For retrieving the above data, refer to the CLI and server pages:
|
||||
href="/docs/usage/server"
|
||||
/>
|
||||
</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.
|
||||
|
||||
@@ -12,52 +12,36 @@ Cup's CLI provides the `cup check` command.
|
||||
|
||||
```ansi
|
||||
$ cup check
|
||||
[32;1m✓[0m Done!
|
||||
[90;1m~ Local images[0m
|
||||
[90;1m╭─────────────────────────────────────────┬──────────────────────────────────┬─────────╮[0m
|
||||
[90;1m│[36;1mReference [90;1m│[36;1mStatus [90;1m│[36;1mTime (ms)[90;1m│[0m
|
||||
[90;1m├─────────────────────────────────────────┼──────────────────────────────────┼─────────┤[0m
|
||||
[90;1m│[0mpostgres:15-alpine [90;1m│[0m[31mMajor update (15 → 17) [0m[90;1m│[0m788 [90;1m│[0m
|
||||
[90;1m│[0mghcr.io/immich-app/immich-server:v1.118.2[90;1m│[0m[33mMinor update (1.118.2 → 1.127.0) [0m[90;1m│[0m2294 [90;1m│[0m
|
||||
[90;1m│[0mollama/ollama:0.4.1 [90;1m│[0m[33mMinor update (0.4.1 → 0.5.12) [0m[90;1m│[0m533 [90;1m│[0m
|
||||
[90;1m│[0madguard/adguardhome:v0.107.52 [90;1m│[0m[34mPatch update (0.107.52 → 0.107.57)[0m[90;1m│[0m1738 [90;1m│[0m
|
||||
[90;1m│[0mjc21/nginx-proxy-manager:latest [90;1m│[0m[32mUp to date [0m[90;1m│[0m583 [90;1m│[0m
|
||||
[90;1m│[0mlouislam/uptime-kuma:1 [90;1m│[0m[32mUp to date [0m[90;1m│[0m793 [90;1m│[0m
|
||||
[90;1m│[0mmoby/buildkit:buildx-stable-1 [90;1m│[0m[32mUp to date [0m[90;1m│[0m600 [90;1m│[0m
|
||||
[90;1m│[0mtecnativa/docker-socket-proxy:latest [90;1m│[0m[32mUp to date [0m[90;1m│[0m564 [90;1m│[0m
|
||||
[90;1m│[0mubuntu:latest [90;1m│[0m[32mUp to date [0m[90;1m│[0m585 [90;1m│[0m
|
||||
[90;1m│[0mwagoodman/dive:latest [90;1m│[0m[32mUp to date [0m[90;1m│[0m585 [90;1m│[0m
|
||||
[90;1m│[0mrolebot:latest [90;1m│[0m[90mUnknown [0m[90;1m│[0m174 [90;1m│[0m
|
||||
[90;1m╰─────────────────────────────────────────┴──────────────────────────────────┴─────────╯[0m
|
||||
[36;1m INFO[0m ✨ Checked 11 images in 8312ms
|
||||
[38;5;1m
|
||||
mysql:8.0 Major update
|
||||
node:20 Major update
|
||||
postgres:16-alpine Major update[0m[38;5;3m
|
||||
rust:1.80.1-alpine Minor update[0m[38;5;12m
|
||||
redis:7.4.0 Patch update
|
||||
nginx:alpine Update available
|
||||
redis:alpine Update available
|
||||
ubuntu:latest Update available[0m[38;5;2m
|
||||
node:iron Up to date
|
||||
2fauth/2fauth:latest Up to date
|
||||
c1982/sdns:latest Up to date[0m[38;5;8m
|
||||
registry.acme.com/acme-server:latest Unknown
|
||||
[36;1mINFO [0m✨ Checked 58 images in 3772ms
|
||||
```
|
||||
|
||||
### Check for updates to specific images
|
||||
|
||||
```ansi
|
||||
$ cup check node:latest
|
||||
[32;1m✓[0m Done!
|
||||
[90;1m~ Local images[0m
|
||||
[90;1m╭───────────┬────────────────┬─────────╮[0m
|
||||
[90;1m│[36;1mReference [90;1m│[36;1mStatus [90;1m│[36;1mTime (ms)[90;1m│[0m
|
||||
[90;1m├───────────┼────────────────┼─────────┤[0m
|
||||
[90;1m│[0mnode:latest[90;1m│[0m[34mUpdate available[0m[90;1m│[0m788 [90;1m│[0m
|
||||
[90;1m╰───────────┴────────────────┴─────────╯[0m
|
||||
[36;1m INFO[0m ✨ Checked 1 images in 310ms
|
||||
$ cup check node:latest[38;5;12m
|
||||
node:latest Update available
|
||||
[36;1mINFO [0m✨ Checked 1 images in 1310ms
|
||||
```
|
||||
|
||||
```ansi
|
||||
$ cup check nextcloud:30 postgres:14 mysql:8.0[38;5;12m
|
||||
[32;1m✓[0m Done!
|
||||
[90;1m~ Local images[0m
|
||||
[90;1m╭────────────┬────────────────────────┬─────────╮[0m
|
||||
[90;1m│[36;1mReference [90;1m│[36;1mStatus [90;1m│[36;1mTime (ms)[90;1m│[0m
|
||||
[90;1m├────────────┼────────────────────────┼─────────┤[0m
|
||||
[90;1m│[0mpostgres:14 [90;1m│[0m[31mMajor update (14 → 17) [0m[90;1m│[0m195 [90;1m│[0m
|
||||
[90;1m│[0mmysql:8.0 [90;1m│[0m[31mMajor update (8.0 → 9.2)[0m[90;1m│[0m382 [90;1m│[0m
|
||||
[90;1m│[0mnextcloud:30[90;1m│[0m[32mUp to date [0m[90;1m│[0m585 [90;1m│[0m
|
||||
[90;1m╰────────────┴────────────────────────┴─────────╯[0m
|
||||
[36;1m INFO[0m ✨ Checked 3 images in 769ms
|
||||
nextcloud:30 Update available
|
||||
postgres:14 Update available[38;5;2m
|
||||
mysql:8.0 Up to date
|
||||
[36;1mINFO [0m✨ Checked 3 images in 1769ms
|
||||
```
|
||||
|
||||
## Enable icons
|
||||
@@ -78,8 +62,7 @@ $ cup check -r
|
||||
```
|
||||
|
||||
<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>
|
||||
|
||||
## Usage with Docker
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Cards } from "nextra/components";
|
||||
|
||||
# Usage
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
<Cards>
|
||||
<Cards.Card icon={<IconTerminal />} title="CLI" href="/docs/usage/cli" />
|
||||
|
||||
@@ -8,13 +8,13 @@ The server provides the `cup serve` command.
|
||||
|
||||
```ansi
|
||||
$ cup serve
|
||||
[36;1m INFO [0mStarting server, please wait...
|
||||
[36;1m INFO [0m✨ Checked 8 images in 8862ms
|
||||
[36;1m INFO [0mReady to start!
|
||||
[94;1m HTTP [0m[32mGET[0m / [32m200[0m in 0ms
|
||||
[94;1m HTTP [0m[32mGET[0m /assets/index.js [32m200[0m in 3ms
|
||||
[94;1m HTTP [0m[32mGET[0m /assets/index.css [32m200[0m in 0ms
|
||||
[94;1m HTTP [0m[32mGET[0m /api/v3/json [32m200[0m in 0ms
|
||||
[36;1mINFO [0mStarting server, please wait...
|
||||
[36;1mINFO [0m✨ Checked 8 images in 8862ms
|
||||
[36;1mINFO [0mReady to start!
|
||||
[94;1mHTTP [0m[32mGET[0m / [32m200[0m in 0ms
|
||||
[94;1mHTTP [0m[32mGET[0m /assets/index.js [32m200[0m in 3ms
|
||||
[94;1mHTTP [0m[32mGET[0m /assets/index.css [32m200[0m in 0ms
|
||||
[94;1mHTTP [0m[32mGET[0m /api/v3/json [32m200[0m 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.)
|
||||
@@ -29,13 +29,13 @@ Pass the `-p` argument with the port you want to use
|
||||
|
||||
```ansi
|
||||
$ cup serve -p 9000
|
||||
[36;1m INFO [0mStarting server, please wait...
|
||||
[36;1m INFO [0m✨ Checked 8 images in 8862ms
|
||||
[36;1m INFO [0mReady to start!
|
||||
[94;1m HTTP [0m[32mGET[0m / [32m200[0m in 0ms
|
||||
[94;1m HTTP [0m[32mGET[0m /assets/index.js [32m200[0m in 3ms
|
||||
[94;1m HTTP [0m[32mGET[0m /assets/index.css [32m200[0m in 0ms
|
||||
[94;1m HTTP [0m[32mGET[0m /api/v3/json [32m200[0m in 0ms
|
||||
[36;1mINFO [0mStarting server, please wait...
|
||||
[36;1mINFO [0m✨ Checked 8 images in 8862ms
|
||||
[36;1mINFO [0mReady to start!
|
||||
[94;1mHTTP [0m[32mGET[0m / [32m200[0m in 0ms
|
||||
[94;1mHTTP [0m[32mGET[0m /assets/index.js [32m200[0m in 3ms
|
||||
[94;1mHTTP [0m[32mGET[0m /assets/index.css [32m200[0m in 0ms
|
||||
[94;1mHTTP [0m[32mGET[0m /api/v3/json [32m200[0m in 0ms
|
||||
```
|
||||
|
||||
## Usage with Docker
|
||||
|
||||
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 90 KiB |
62
src/check.rs
@@ -3,7 +3,7 @@ use itertools::Itertools;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use crate::{
|
||||
docker::{get_images_from_docker_daemon, get_in_use_images},
|
||||
docker::get_images_from_docker_daemon,
|
||||
http::Client,
|
||||
registry::{check_auth, get_token},
|
||||
structs::{image::Image, update::Update},
|
||||
@@ -17,7 +17,7 @@ async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Ve
|
||||
|
||||
let handles: Vec<_> = ctx.config.servers
|
||||
.iter()
|
||||
.map(|(name, url)| async move {
|
||||
.map(|(name, url)| async {
|
||||
let base_url = if url.starts_with("http://") || url.starts_with("https://") {
|
||||
format!("{}/api/v3/", url.trim_end_matches('/'))
|
||||
} else {
|
||||
@@ -26,10 +26,10 @@ async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Ve
|
||||
let json_url = base_url.clone() + "json";
|
||||
if refresh {
|
||||
let refresh_url = base_url + "refresh";
|
||||
match client.get(&refresh_url, &[], false).await {
|
||||
match client.get(&(&refresh_url), vec![], false).await {
|
||||
Ok(response) => {
|
||||
if response.status() != 200 {
|
||||
ctx.logger.warn(format!("GET {}: Failed to refresh server. Server returned invalid response code: {}", refresh_url, response.status()));
|
||||
ctx.logger.warn(format!("GET {}: Failed to refresh server. Server returned invalid response code: {}",refresh_url,response.status()));
|
||||
return Vec::new();
|
||||
}
|
||||
},
|
||||
@@ -40,14 +40,13 @@ async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Ve
|
||||
}
|
||||
|
||||
}
|
||||
match client.get(&json_url, &[], false).await {
|
||||
match client.get(&json_url, vec![], false).await {
|
||||
Ok(response) => {
|
||||
if response.status() != 200 {
|
||||
ctx.logger.warn(format!("GET {}: Failed to fetch updates from server. Server returned invalid response code: {}", json_url, response.status()));
|
||||
ctx.logger.warn(format!("GET {}: Failed to fetch updates from server. Server returned invalid response code: {}",json_url,response.status()));
|
||||
return Vec::new();
|
||||
}
|
||||
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() {
|
||||
let mut server_updates: Vec<Update> = updates
|
||||
.iter()
|
||||
@@ -58,7 +57,6 @@ async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Ve
|
||||
update.server = Some(name.clone());
|
||||
update.status = update.get_status();
|
||||
}
|
||||
ctx.logger.debug(format!("Updates for {}: {:#?}", name, server_updates));
|
||||
return server_updates;
|
||||
}
|
||||
|
||||
@@ -81,41 +79,20 @@ async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Ve
|
||||
|
||||
/// Returns a list of updates for all images passed in.
|
||||
pub async fn get_updates(
|
||||
references: &Option<Vec<String>>, // If a user requested _specific_ references to be checked, this will have a value
|
||||
references: &Option<Vec<String>>,
|
||||
refresh: bool,
|
||||
ctx: &Context,
|
||||
) -> Vec<Update> {
|
||||
let client = Client::new(ctx);
|
||||
|
||||
// Merge references argument with references from config
|
||||
let all_references = match &references {
|
||||
Some(refs) => {
|
||||
if !ctx.config.images.extra.is_empty() {
|
||||
refs.clone().extend_from_slice(&ctx.config.images.extra);
|
||||
}
|
||||
refs
|
||||
}
|
||||
None => &ctx.config.images.extra,
|
||||
};
|
||||
|
||||
// Get local images
|
||||
ctx.logger.debug("Retrieving images to be checked");
|
||||
let mut images = get_images_from_docker_daemon(ctx, references).await;
|
||||
let in_use_images = get_in_use_images(ctx).await;
|
||||
ctx.logger
|
||||
.debug(format!("Found {} images in use", in_use_images.len()));
|
||||
|
||||
// Complete in_use field
|
||||
images.iter_mut().for_each(|image| {
|
||||
if in_use_images.contains(&image.reference) {
|
||||
image.in_use = true
|
||||
}
|
||||
});
|
||||
|
||||
// Add extra images from references
|
||||
if !all_references.is_empty() {
|
||||
if let Some(refs) = references {
|
||||
let image_refs: FxHashSet<&String> = images.iter().map(|image| &image.reference).collect();
|
||||
let extra = all_references
|
||||
let extra = refs
|
||||
.iter()
|
||||
.filter(|&reference| !image_refs.contains(reference))
|
||||
.map(|reference| Image::from_reference(reference))
|
||||
@@ -141,10 +118,6 @@ pub async fn get_updates(
|
||||
.iter()
|
||||
.map(|image| &image.parts.registry)
|
||||
.unique()
|
||||
.filter(|®istry| match ctx.config.registries.get(registry) {
|
||||
Some(config) => !config.ignore,
|
||||
None => true,
|
||||
})
|
||||
.collect::<Vec<&String>>();
|
||||
|
||||
// Create request client. All network requests share the same client for better performance.
|
||||
@@ -163,7 +136,7 @@ pub async fn get_updates(
|
||||
|
||||
// Retrieve an authentication token (if required) for each registry.
|
||||
let mut tokens: FxHashMap<&str, Option<String>> = FxHashMap::default();
|
||||
for registry in registries.clone() {
|
||||
for registry in registries {
|
||||
let credentials = if let Some(registry_config) = ctx.config.registries.get(registry) {
|
||||
®istry_config.authentication
|
||||
} else {
|
||||
@@ -188,11 +161,24 @@ pub async fn get_updates(
|
||||
|
||||
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());
|
||||
|
||||
// Loop through images check for updates
|
||||
for image in &images {
|
||||
let is_ignored = !registries.contains(&&image.parts.registry)
|
||||
let is_ignored = ignored_registries.contains(&&image.parts.registry)
|
||||
|| ctx
|
||||
.config
|
||||
.images
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use std::env;
|
||||
use std::mem;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::error;
|
||||
|
||||
// We can't assign `a` to `b` in the loop in `Config::load`, so we'll have to use swap. It looks ugly so now we have a macro for it.
|
||||
macro_rules! swap {
|
||||
($a:expr, $b:expr) => {
|
||||
mem::swap(&mut $a, &mut $b)
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Theme {
|
||||
#[serde(rename = "default")]
|
||||
Default,
|
||||
#[serde(rename = "blue")]
|
||||
Blue,
|
||||
}
|
||||
|
||||
@@ -27,21 +19,6 @@ impl Default for Theme {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UpdateType {
|
||||
None,
|
||||
Major,
|
||||
Minor,
|
||||
Patch,
|
||||
}
|
||||
|
||||
impl Default for UpdateType {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(default)]
|
||||
@@ -63,9 +40,7 @@ pub struct ImageConfig {
|
||||
pub struct Config {
|
||||
version: u8,
|
||||
pub agent: bool,
|
||||
pub ignore_update_type: UpdateType,
|
||||
pub images: ImageConfig,
|
||||
#[serde(deserialize_with = "empty_as_none")]
|
||||
pub refresh_interval: Option<String>,
|
||||
pub registries: FxHashMap<String, RegistryConfig>,
|
||||
pub servers: FxHashMap<String, String>,
|
||||
@@ -78,7 +53,6 @@ impl Config {
|
||||
Self {
|
||||
version: 3,
|
||||
agent: false,
|
||||
ignore_update_type: UpdateType::default(),
|
||||
images: ImageConfig::default(),
|
||||
refresh_interval: None,
|
||||
registries: FxHashMap::default(),
|
||||
@@ -88,41 +62,8 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads file and env config and merges them
|
||||
pub fn load(&mut self, path: Option<PathBuf>) -> Self {
|
||||
let mut config = self.load_file(path);
|
||||
|
||||
// Get environment variables with CUP_ prefix
|
||||
let env_vars: FxHashMap<String, String> =
|
||||
env::vars().filter(|(k, _)| k.starts_with("CUP_")).collect();
|
||||
|
||||
if !env_vars.is_empty() {
|
||||
if let Ok(mut cfg) = envy::prefixed("CUP_").from_env::<Config>() {
|
||||
// If we have environment variables, override config.json options
|
||||
for (key, _) in env_vars {
|
||||
match key.as_str() {
|
||||
"CUP_AGENT" => config.agent = cfg.agent,
|
||||
#[rustfmt::skip]
|
||||
"CUP_IGNORE_UPDATE_TYPE" => swap!(config.ignore_update_type, cfg.ignore_update_type),
|
||||
#[rustfmt::skip]
|
||||
"CUP_REFRESH_INTERVAL" => swap!(config.refresh_interval, cfg.refresh_interval),
|
||||
"CUP_SOCKET" => swap!(config.socket, cfg.socket),
|
||||
"CUP_THEME" => swap!(config.theme, cfg.theme),
|
||||
// The syntax for these is slightly more complicated, not sure if they should be enabled or not. Let's stick to simple types for now.
|
||||
// "CUP_IMAGES" => swap!(config.images, cfg.images),
|
||||
// "CUP_REGISTRIES" => swap!(config.registries, cfg.registries),
|
||||
// "CUP_SERVERS" => swap!(config.servers, cfg.servers),
|
||||
_ => (), // Maybe print a warning if other CUP_ variables are detected
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// Reads the config from the file path provided and returns the parsed result.
|
||||
fn load_file(&self, path: Option<PathBuf>) -> Self {
|
||||
pub fn load(&self, path: Option<PathBuf>) -> Self {
|
||||
let raw_config = match &path {
|
||||
Some(path) => std::fs::read_to_string(path),
|
||||
None => return Self::new(), // Empty config
|
||||
@@ -136,7 +77,7 @@ impl Config {
|
||||
self.parse(&raw_config.unwrap()) // We can safely unwrap here
|
||||
}
|
||||
/// Parses and validates the config.
|
||||
fn parse(&self, raw_config: &str) -> Self {
|
||||
pub fn parse(&self, raw_config: &str) -> Self {
|
||||
let config: Self = match serde_json::from_str(raw_config) {
|
||||
Ok(config) => config,
|
||||
Err(e) => error!("Unexpected error occured while parsing config: {}", e),
|
||||
@@ -153,15 +94,3 @@ impl Default for Config {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_as_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
if s.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(s))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use bollard::{container::ListContainersOptions, models::ImageInspect, ClientVersion, Docker};
|
||||
use bollard::{models::ImageInspect, ClientVersion, Docker};
|
||||
|
||||
use futures::future::join_all;
|
||||
|
||||
@@ -42,26 +42,7 @@ pub async fn get_images_from_docker_daemon(
|
||||
references: &Option<Vec<String>>,
|
||||
) -> Vec<Image> {
|
||||
let client: Docker = create_docker_client(ctx.config.socket.as_deref());
|
||||
let mut swarm_images = match client.list_services::<String>(None).await {
|
||||
Ok(services) => services
|
||||
.iter()
|
||||
.filter_map(|service| match &service.spec {
|
||||
Some(service_spec) => match &service_spec.task_template {
|
||||
Some(task_spec) => match &task_spec.container_spec {
|
||||
Some(container_spec) => match &container_spec.image {
|
||||
Some(image) => Image::from_inspect_data(ctx, image),
|
||||
None => None,
|
||||
},
|
||||
None => None,
|
||||
},
|
||||
None => None,
|
||||
},
|
||||
None => None,
|
||||
})
|
||||
.collect(),
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
let mut local_images = match references {
|
||||
match references {
|
||||
Some(refs) => {
|
||||
let mut inspect_handles = Vec::with_capacity(refs.len());
|
||||
for reference in refs {
|
||||
@@ -75,7 +56,7 @@ pub async fn get_images_from_docker_daemon(
|
||||
.collect();
|
||||
inspects
|
||||
.iter()
|
||||
.filter_map(|inspect| Image::from_inspect_data(ctx, inspect.clone()))
|
||||
.filter_map(|inspect| Image::from_inspect_data(inspect.clone()))
|
||||
.collect()
|
||||
}
|
||||
None => {
|
||||
@@ -87,41 +68,8 @@ pub async fn get_images_from_docker_daemon(
|
||||
};
|
||||
images
|
||||
.iter()
|
||||
.filter_map(|image| Image::from_inspect_data(ctx, image.clone()))
|
||||
.collect::<Vec<Image>>()
|
||||
.filter_map(|image| Image::from_inspect_data(image.clone()))
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
local_images.append(&mut swarm_images);
|
||||
local_images
|
||||
}
|
||||
|
||||
pub async fn get_in_use_images(ctx: &Context) -> Vec<String> {
|
||||
let client: Docker = create_docker_client(ctx.config.socket.as_deref());
|
||||
|
||||
let containers = match client
|
||||
.list_containers::<String>(Some(ListContainersOptions {
|
||||
all: true,
|
||||
..Default::default()
|
||||
}))
|
||||
.await
|
||||
{
|
||||
Ok(containers) => containers,
|
||||
Err(e) => {
|
||||
error!("Failed to retrieve list of containers available!\n{}", e)
|
||||
}
|
||||
};
|
||||
|
||||
containers
|
||||
.iter()
|
||||
.filter_map(|container| match &container.image {
|
||||
Some(image) => Some({
|
||||
if image.contains(":") {
|
||||
image.clone()
|
||||
} else {
|
||||
format!("{image}:latest")
|
||||
}
|
||||
}),
|
||||
None => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,11 +124,11 @@ pub fn print_updates(updates: &[Update], icons: &bool) {
|
||||
Status::Unknown(_) => "\x1b[90m",
|
||||
};
|
||||
let description = format!(
|
||||
"{}{}",
|
||||
"{} {}",
|
||||
status,
|
||||
match &update.result.info {
|
||||
UpdateInfo::Version(info) => {
|
||||
format!(" ({} → {})", info.current_version, info.new_version)
|
||||
format!("({} → {})", info.current_version, info.new_version)
|
||||
}
|
||||
_ => String::new(),
|
||||
}
|
||||
|
||||
16
src/http.rs
@@ -42,7 +42,7 @@ impl Client {
|
||||
&self,
|
||||
url: &str,
|
||||
method: RequestMethod,
|
||||
headers: &[(&str, Option<&str>)],
|
||||
headers: Vec<(&str, Option<&str>)>,
|
||||
ignore_401: bool,
|
||||
) -> Result<Response, String> {
|
||||
let mut request = match method {
|
||||
@@ -51,7 +51,7 @@ impl Client {
|
||||
};
|
||||
for (name, value) in headers {
|
||||
if let Some(v) = value {
|
||||
request = request.header(*name, *v)
|
||||
request = request.header(name, v)
|
||||
}
|
||||
}
|
||||
match request.send().await {
|
||||
@@ -69,10 +69,6 @@ impl Client {
|
||||
self.ctx.logger.warn(&message);
|
||||
Err(message)
|
||||
}
|
||||
} else if status == 502 {
|
||||
let message = format!("{} {}: The registry is currently unavailabile (returned status code 502).", method, url);
|
||||
self.ctx.logger.warn(&message);
|
||||
Err(message)
|
||||
} else if status.as_u16() <= 400 {
|
||||
Ok(response)
|
||||
} else {
|
||||
@@ -99,10 +95,6 @@ impl Client {
|
||||
let message = format!("{} {}: Connection timed out!", method, url);
|
||||
self.ctx.logger.warn(&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 {
|
||||
error!(
|
||||
"{} {}: Unexpected error: {}",
|
||||
@@ -118,7 +110,7 @@ impl Client {
|
||||
pub async fn get(
|
||||
&self,
|
||||
url: &str,
|
||||
headers: &[(&str, Option<&str>)],
|
||||
headers: Vec<(&str, Option<&str>)>,
|
||||
ignore_401: bool,
|
||||
) -> Result<Response, String> {
|
||||
self.request(url, RequestMethod::GET, headers, ignore_401)
|
||||
@@ -128,7 +120,7 @@ impl Client {
|
||||
pub async fn head(
|
||||
&self,
|
||||
url: &str,
|
||||
headers: &[(&str, Option<&str>)],
|
||||
headers: Vec<(&str, Option<&str>)>,
|
||||
) -> Result<Response, String> {
|
||||
self.request(url, RequestMethod::HEAD, headers, false).await
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::time::SystemTime;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::{
|
||||
config::UpdateType,
|
||||
error,
|
||||
http::Client,
|
||||
structs::{
|
||||
@@ -23,7 +22,7 @@ use crate::{
|
||||
pub async fn check_auth(registry: &str, ctx: &Context, client: &Client) -> Option<String> {
|
||||
let protocol = get_protocol(registry, &ctx.config.registries);
|
||||
let url = format!("{}://{}/v2/", protocol, registry);
|
||||
let response = client.get(&url, &[], true).await;
|
||||
let response = client.get(&url, Vec::new(), true).await;
|
||||
match response {
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
@@ -58,9 +57,9 @@ pub async fn get_latest_digest(
|
||||
protocol, &image.parts.registry, &image.parts.repository, &image.parts.tag
|
||||
);
|
||||
let authorization = to_bearer_string(&token);
|
||||
let headers = [("Accept", Some("application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json")), ("Authorization", authorization.as_deref())];
|
||||
let headers = vec![("Accept", Some("application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json")), ("Authorization", authorization.as_deref())];
|
||||
|
||||
let response = client.head(&url, &headers).await;
|
||||
let response = client.head(&url, headers).await;
|
||||
let time = start.elapsed().unwrap().as_millis() as u32;
|
||||
ctx.logger.debug(format!(
|
||||
"Checked for digest update to {} in {}ms",
|
||||
@@ -96,7 +95,7 @@ pub async fn get_latest_digest(
|
||||
}
|
||||
|
||||
pub async fn get_token(
|
||||
images: &[&Image],
|
||||
images: &Vec<&Image>,
|
||||
auth_url: &str,
|
||||
credentials: &Option<String>,
|
||||
client: &Client,
|
||||
@@ -106,9 +105,9 @@ pub async fn get_token(
|
||||
url = format!("{}&scope=repository:{}:pull", url, image.parts.repository);
|
||||
}
|
||||
let authorization = credentials.as_ref().map(|creds| format!("Basic {}", creds));
|
||||
let headers = [("Authorization", authorization.as_deref())];
|
||||
let headers = vec![("Authorization", authorization.as_deref())];
|
||||
|
||||
let response = client.get(&url, &headers, false).await;
|
||||
let response = client.get(&url, headers, false).await;
|
||||
let response_json = match response {
|
||||
Ok(response) => parse_json(&get_response_body(response).await),
|
||||
Err(_) => error!("GET {}: Request failed!", url),
|
||||
@@ -132,7 +131,7 @@ pub async fn get_latest_tag(
|
||||
protocol, &image.parts.registry, &image.parts.repository,
|
||||
);
|
||||
let authorization = to_bearer_string(&token);
|
||||
let headers = [
|
||||
let headers = vec![
|
||||
("Accept", Some("application/json")),
|
||||
("Authorization", authorization.as_deref()),
|
||||
];
|
||||
@@ -148,10 +147,9 @@ pub async fn get_latest_tag(
|
||||
));
|
||||
let (new_tags, next) = match get_extra_tags(
|
||||
&next_url.unwrap(),
|
||||
&headers,
|
||||
headers.clone(),
|
||||
base,
|
||||
&image.version_info.as_ref().unwrap().format_str,
|
||||
ctx,
|
||||
client,
|
||||
)
|
||||
.await
|
||||
@@ -184,7 +182,10 @@ pub async fn get_latest_tag(
|
||||
));
|
||||
get_latest_digest(
|
||||
&Image {
|
||||
version_info: None, // Overwrite previous version info, since it isn't useful anymore (equal tags means up to date and an image is truly up to date when its digests are up to date, and we'll be checking those anyway)
|
||||
version_info: Some(VersionInfo {
|
||||
latest_remote_tag: Some(t.clone()),
|
||||
..image.version_info.as_ref().unwrap().clone()
|
||||
}),
|
||||
time_ms: image.time_ms + elapsed(start),
|
||||
..image.clone()
|
||||
},
|
||||
@@ -204,19 +205,15 @@ pub async fn get_latest_tag(
|
||||
}
|
||||
}
|
||||
}
|
||||
None => error!(
|
||||
"Image {} has no remote version tags! Local tag: {}",
|
||||
image.reference, image.parts.tag
|
||||
),
|
||||
None => unreachable!("{:?}", tags),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_extra_tags(
|
||||
url: &str,
|
||||
headers: &[(&str, Option<&str>)],
|
||||
headers: Vec<(&str, Option<&str>)>,
|
||||
base: &Version,
|
||||
format_str: &str,
|
||||
ctx: &Context,
|
||||
client: &Client,
|
||||
) -> Result<(Vec<Version>, Option<String>), String> {
|
||||
let response = client.get(url, headers, false).await;
|
||||
@@ -240,18 +237,7 @@ pub async fn get_extra_tags(
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.filter_map(|(tag, _)| match ctx.config.ignore_update_type {
|
||||
UpdateType::None => Some(tag),
|
||||
UpdateType::Major => Some(tag).filter(|tag| base.major == tag.major),
|
||||
UpdateType::Minor => {
|
||||
Some(tag).filter(|tag| base.major == tag.major && base.minor == tag.minor)
|
||||
}
|
||||
UpdateType::Patch => Some(tag).filter(|tag| {
|
||||
base.major == tag.major
|
||||
&& base.minor == tag.minor
|
||||
&& base.patch == tag.patch
|
||||
}),
|
||||
})
|
||||
.map(|(tag, _)| tag)
|
||||
.dedup()
|
||||
.collect();
|
||||
Ok((result, next_url))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{env, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::Local;
|
||||
use chrono_tz::Tz;
|
||||
use liquid::{object, Object, ValueView};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde_json::Value;
|
||||
@@ -9,7 +8,6 @@ use tokio::sync::Mutex;
|
||||
use tokio_cron_scheduler::{Job, JobScheduler};
|
||||
use xitca_web::{
|
||||
body::ResponseBody,
|
||||
bytes::Bytes,
|
||||
error::Error,
|
||||
handler::{handler_service, path::PathRef, state::StateRef},
|
||||
http::{StatusCode, WebResponse},
|
||||
@@ -21,7 +19,6 @@ use xitca_web::{
|
||||
use crate::{
|
||||
check::get_updates,
|
||||
config::Theme,
|
||||
error,
|
||||
structs::update::Update,
|
||||
utils::{
|
||||
json::{to_full_json, to_simple_json},
|
||||
@@ -34,9 +31,9 @@ use crate::{
|
||||
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: Bytes = Bytes::from_static(include_bytes!("static/favicon.ico"));
|
||||
const FAVICON_SVG: Bytes = Bytes::from_static(include_bytes!("static/favicon.svg"));
|
||||
const APPLE_TOUCH_ICON: Bytes = Bytes::from_static(include_bytes!("static/apple-touch-icon.png"));
|
||||
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");
|
||||
|
||||
const SORT_ORDER: [&str; 8] = [
|
||||
"monitored_images",
|
||||
@@ -55,34 +52,16 @@ pub async fn serve(port: &u16, ctx: &Context) -> std::io::Result<()> {
|
||||
let scheduler = JobScheduler::new().await.unwrap();
|
||||
let data = Arc::new(Mutex::new(data));
|
||||
let data_copy = data.clone();
|
||||
let tz = env::var("TZ")
|
||||
.map(|tz| tz.parse().unwrap_or(Tz::UTC))
|
||||
.unwrap_or(Tz::UTC);
|
||||
if let Some(interval) = &ctx.config.refresh_interval {
|
||||
scheduler
|
||||
.add(
|
||||
match Job::new_async_tz(
|
||||
interval,
|
||||
tz,
|
||||
move |_uuid, _lock| {
|
||||
let data_copy = data_copy.clone();
|
||||
Box::pin(async move {
|
||||
data_copy.lock().await.refresh().await;
|
||||
})
|
||||
},
|
||||
) {
|
||||
Ok(job) => job,
|
||||
Err(e) => match e {
|
||||
tokio_cron_scheduler::JobSchedulerError::ParseSchedule => error!(
|
||||
"Failed to parse cron schedule: {}. Please ensure it is valid!",
|
||||
interval
|
||||
),
|
||||
e => error!(
|
||||
"An unexpected error occured while scheduling automatic refresh: {}",
|
||||
e
|
||||
),
|
||||
},
|
||||
},
|
||||
Job::new_async(interval, move |_uuid, _lock| {
|
||||
let data_copy = data_copy.clone();
|
||||
Box::pin(async move {
|
||||
data_copy.lock().await.refresh().await;
|
||||
})
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -100,16 +79,12 @@ pub async fn serve(port: &u16, ctx: &Context) -> std::io::Result<()> {
|
||||
.at("/", get(handler_service(_static)))
|
||||
.at("/*", get(handler_service(_static)));
|
||||
}
|
||||
match app_builder
|
||||
app_builder
|
||||
.enclosed_fn(logger)
|
||||
.serve()
|
||||
.bind(format!("0.0.0.0:{}", port))
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(_) => error!("Failed to bind to port {}. Is it in use?", port),
|
||||
}
|
||||
.run()
|
||||
.wait()
|
||||
.bind(format!("0.0.0.0:{}", port))?
|
||||
.run()
|
||||
.wait()
|
||||
}
|
||||
|
||||
async fn _static(data: StateRef<'_, Arc<Mutex<ServerData>>>, path: PathRef<'_>) -> WebResponse {
|
||||
|
||||
@@ -35,40 +35,24 @@ pub struct VersionInfo {
|
||||
pub struct Image {
|
||||
pub reference: String,
|
||||
pub parts: Parts,
|
||||
pub url: Option<String>,
|
||||
pub digest_info: Option<DigestInfo>,
|
||||
pub version_info: Option<VersionInfo>,
|
||||
pub in_use: bool,
|
||||
pub error: Option<String>,
|
||||
pub time_ms: u32,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
/// Creates and populates the fields of an Image object based on the ImageSummary from the Docker daemon
|
||||
pub fn from_inspect_data<T: InspectData>(ctx: &Context, image: T) -> Option<Self> {
|
||||
pub fn from_inspect_data<T: InspectData>(image: T) -> Option<Self> {
|
||||
let tags = image.tags().unwrap();
|
||||
let digests = image.digests().unwrap();
|
||||
if !tags.is_empty() && !digests.is_empty() {
|
||||
let reference = tags[0].clone();
|
||||
if reference.contains('@') {
|
||||
return None; // As far as I know, references that contain @ are either manually pulled by the user or automatically created because of swarm. In the first case AFAICT we can't know what tag was originally pulled, so we'd have to make assumptions and I've decided to remove this. The other case is already handled seperately, so this also ensures images aren't displayed twice, once with and once without a digest.
|
||||
};
|
||||
let (registry, repository, tag) = split(&reference);
|
||||
let version_tag = Version::from_tag(&tag);
|
||||
let local_digests = digests
|
||||
.iter()
|
||||
.filter_map(
|
||||
|digest| match digest.split('@').collect::<Vec<&str>>().get(1) {
|
||||
Some(digest) => Some(digest.to_string()),
|
||||
None => {
|
||||
ctx.logger.warn(format!(
|
||||
"Ignoring invalid digest {} for image {}!",
|
||||
digest, reference
|
||||
));
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.map(|digest| digest.split('@').collect::<Vec<&str>>()[1].to_string())
|
||||
.collect();
|
||||
Some(Self {
|
||||
reference,
|
||||
@@ -77,7 +61,6 @@ impl Image {
|
||||
repository,
|
||||
tag,
|
||||
},
|
||||
url: image.url(),
|
||||
digest_info: Some(DigestInfo {
|
||||
local_digests,
|
||||
remote_digest: None,
|
||||
@@ -158,7 +141,6 @@ impl Image {
|
||||
Update {
|
||||
reference: self.reference.clone(),
|
||||
parts: self.parts.clone(),
|
||||
url: self.url.clone(),
|
||||
result: UpdateResult {
|
||||
has_update: has_update.to_option_bool(),
|
||||
info: match has_update {
|
||||
@@ -222,7 +204,6 @@ impl Image {
|
||||
},
|
||||
time: self.time_ms,
|
||||
server: None,
|
||||
in_use: self.in_use,
|
||||
status: has_update,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,26 @@
|
||||
use bollard::secret::{ImageInspect, ImageSummary};
|
||||
|
||||
pub trait InspectData {
|
||||
fn tags(&self) -> Option<Vec<String>>;
|
||||
fn digests(&self) -> Option<Vec<String>>;
|
||||
fn url(&self) -> Option<String>;
|
||||
fn tags(&self) -> Option<&Vec<String>>;
|
||||
fn digests(&self) -> Option<&Vec<String>>;
|
||||
}
|
||||
|
||||
impl InspectData for ImageInspect {
|
||||
fn tags(&self) -> Option<Vec<String>> {
|
||||
self.repo_tags.clone()
|
||||
fn tags(&self) -> Option<&Vec<String>> {
|
||||
self.repo_tags.as_ref()
|
||||
}
|
||||
|
||||
fn digests(&self) -> Option<Vec<String>> {
|
||||
self.repo_digests.clone()
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
fn digests(&self) -> Option<&Vec<String>> {
|
||||
self.repo_digests.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl InspectData for ImageSummary {
|
||||
fn tags(&self) -> Option<Vec<String>> {
|
||||
Some(self.repo_tags.clone())
|
||||
fn tags(&self) -> Option<&Vec<String>> {
|
||||
Some(&self.repo_tags)
|
||||
}
|
||||
|
||||
fn digests(&self) -> Option<Vec<String>> {
|
||||
Some(self.repo_digests.clone())
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<String> {
|
||||
self.labels.get("org.opencontainers.image.url").cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl InspectData for &String {
|
||||
fn tags(&self) -> Option<Vec<String>> {
|
||||
self.split('@').next().map(|tag| vec![tag.to_string()])
|
||||
}
|
||||
|
||||
fn digests(&self) -> Option<Vec<String>> {
|
||||
match self.split_once('@') {
|
||||
Some((reference, digest)) => Some(vec![format!(
|
||||
"{}@{}",
|
||||
reference.split(':').next().unwrap(),
|
||||
digest
|
||||
)]),
|
||||
None => Some(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<String> {
|
||||
None
|
||||
fn digests(&self) -> Option<&Vec<String>> {
|
||||
Some(&self.repo_digests)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
/// Enum for image status
|
||||
#[derive(Ord, Eq, PartialEq, PartialOrd, Clone, Debug)]
|
||||
#[derive(Ord, Eq, PartialEq, PartialOrd, Clone)]
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
pub enum Status {
|
||||
UpdateMajor,
|
||||
UpdateMinor,
|
||||
|
||||
@@ -2,30 +2,28 @@ use serde::{ser::SerializeStruct, Deserialize, Serialize};
|
||||
|
||||
use super::{parts::Parts, status::Status};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Default))]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq, Debug, Default))]
|
||||
pub struct Update {
|
||||
pub reference: String,
|
||||
pub parts: Parts,
|
||||
pub url: Option<String>,
|
||||
pub result: UpdateResult,
|
||||
pub time: u32,
|
||||
pub server: Option<String>,
|
||||
pub in_use: bool,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub status: Status,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Default))]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq, Debug, Default))]
|
||||
pub struct UpdateResult {
|
||||
pub has_update: Option<bool>,
|
||||
pub info: UpdateInfo,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Default))]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq, Debug, Default))]
|
||||
#[serde(untagged)]
|
||||
pub enum UpdateInfo {
|
||||
#[cfg_attr(test, default)]
|
||||
@@ -34,8 +32,8 @@ pub enum UpdateInfo {
|
||||
Digest(DigestUpdateInfo),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq, Debug))]
|
||||
pub struct VersionUpdateInfo {
|
||||
pub version_update_type: String,
|
||||
pub new_tag: String,
|
||||
@@ -43,8 +41,8 @@ pub struct VersionUpdateInfo {
|
||||
pub new_version: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq, Debug))]
|
||||
pub struct DigestUpdateInfo {
|
||||
pub local_digests: Vec<String>,
|
||||
pub remote_digest: Option<String>,
|
||||
@@ -55,12 +53,10 @@ impl Serialize for VersionUpdateInfo {
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut state = serializer.serialize_struct("VersionUpdateInfo", 5)?;
|
||||
let mut state = serializer.serialize_struct("VersionUpdateInfo", 3)?;
|
||||
let _ = state.serialize_field("type", "version");
|
||||
let _ = state.serialize_field("version_update_type", &self.version_update_type);
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,24 +49,18 @@ impl Version {
|
||||
positions.push((major.start(), major.end()));
|
||||
match major.as_str().parse() {
|
||||
Ok(m) => m,
|
||||
Err(_) => return None,
|
||||
Err(_) => return None
|
||||
}
|
||||
}
|
||||
None => return None,
|
||||
};
|
||||
let minor: Option<u32> = c.name("minor").map(|minor| {
|
||||
positions.push((minor.start(), minor.end()));
|
||||
minor
|
||||
.as_str()
|
||||
.parse()
|
||||
.unwrap_or_else(|_| panic!("Minor version invalid in tag {}", tag))
|
||||
minor.as_str().parse().unwrap_or_else(|_| panic!("Minor version invalid in tag {}", tag))
|
||||
});
|
||||
let patch: Option<u32> = c.name("patch").map(|patch| {
|
||||
positions.push((patch.start(), patch.end()));
|
||||
patch
|
||||
.as_str()
|
||||
.parse()
|
||||
.unwrap_or_else(|_| panic!("Patch version invalid in tag {}", tag))
|
||||
patch.as_str().parse().unwrap_or_else(|_| panic!("Patch version invalid in tag {}", tag))
|
||||
});
|
||||
let mut format_str = tag.to_string();
|
||||
positions.reverse();
|
||||
|
||||
@@ -8,24 +8,15 @@ pub fn split(reference: &str) -> (String, String, String) {
|
||||
0 => unreachable!(),
|
||||
1 => (DEFAULT_REGISTRY, reference.to_string()),
|
||||
_ => {
|
||||
// Check if the image is from Docker Hub
|
||||
if splits[0] == "docker.io" {
|
||||
(DEFAULT_REGISTRY, splits[1..].join("/"))
|
||||
// Check if we're looking at a domain
|
||||
} else if splits[0] == "localhost" || splits[0].contains('.') || splits[0].contains(':')
|
||||
{
|
||||
if splits[0] == "localhost" || splits[0].contains('.') || splits[0].contains(':') {
|
||||
(splits[0], splits[1..].join("/"))
|
||||
} else {
|
||||
(DEFAULT_REGISTRY, reference.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
let splits = repository_and_tag
|
||||
.split('@')
|
||||
.next()
|
||||
.unwrap()
|
||||
.split(':')
|
||||
.collect::<Vec<&str>>();
|
||||
let splits = repository_and_tag.split(':').collect::<Vec<&str>>();
|
||||
let (repository, tag) = match splits.len() {
|
||||
1 | 2 => {
|
||||
let repository_components = splits[0].split('/').collect::<Vec<&str>>();
|
||||
@@ -47,9 +38,7 @@ pub fn split(reference: &str) -> (String, String, String) {
|
||||
};
|
||||
(repository, tag)
|
||||
}
|
||||
_ => {
|
||||
panic!("Failed to parse reference! Splits: {:?}", splits)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
(registry.to_string(), repository, tag.to_string())
|
||||
}
|
||||
@@ -68,7 +57,6 @@ mod tests {
|
||||
assert_eq!(split("localhost:1234/test" ), (String::from("localhost:1234" ), String::from("test" ), String::from("latest")));
|
||||
assert_eq!(split("test:1234/idk" ), (String::from("test:1234" ), String::from("idk" ), String::from("latest")));
|
||||
assert_eq!(split("alpine:3.7" ), (String::from(DEFAULT_REGISTRY ), String::from("library/alpine" ), String::from("3.7" )));
|
||||
assert_eq!(split("docker.io/library/alpine" ), (String::from(DEFAULT_REGISTRY ), String::from("library/alpine" ), String::from("latest")));
|
||||
assert_eq!(split("docker.example.com/examplerepo/alpine:3.7" ), (String::from("docker.example.com" ), String::from("examplerepo/alpine" ), String::from("3.7" )));
|
||||
assert_eq!(split("docker.example.com/examplerepo/alpine/test2:3.7" ), (String::from("docker.example.com" ), String::from("examplerepo/alpine/test2" ), String::from("3.7" )));
|
||||
assert_eq!(split("docker.example.com/examplerepo/alpine/test2/test3:3.7"), (String::from("docker.example.com" ), String::from("examplerepo/alpine/test2/test3"), String::from("3.7" )));
|
||||
|
||||
@@ -17,10 +17,8 @@ pub fn parse_www_authenticate(www_auth: &str) -> String {
|
||||
.fold(String::new(), |acc, (key, value)| {
|
||||
if *key == "realm" {
|
||||
acc.to_owned() + value.as_escaped() + "?"
|
||||
} else if value.unescaped_len() != 0 {
|
||||
format!("{}&{}={}", acc, key, value.as_escaped())
|
||||
} else {
|
||||
acc
|
||||
format!("{}&{}={}", acc, key, value.as_escaped())
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
||||
1
web/.gitignore
vendored
@@ -11,7 +11,6 @@ node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
tsconfig.*.tsbuildinfo
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
1
web/.tool-versions
Normal file
@@ -0,0 +1 @@
|
||||
nodejs 22.8.0
|
||||
705
web/bun.lock
@@ -1,705 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "web",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.2.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"lucide-react": "^0.475.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/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",
|
||||
"prettier-plugin-tailwindcss": "^0.6.8",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.0.1",
|
||||
"vite": "^5.4.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.6.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.1", "", {}, "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.25.1", "", {}, "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.8", "", { "dependencies": { "@eslint/core": "^0.13.0", "levn": "^0.4.1" } }, "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="],
|
||||
|
||||
"@floating-ui/react": ["@floating-ui/react@0.26.28", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw=="],
|
||||
|
||||
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="],
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
|
||||
|
||||
"@headlessui/react": ["@headlessui/react@2.2.2", "", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.17.1", "@react-aria/interactions": "^3.21.3", "@tanstack/react-virtual": "^3.13.6", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-zbniWOYBQ8GHSUIOPY7BbdIn6PzUOq0z41RFrF30HbjsxG6Rrfk+6QulR8Kgf2Vwj2a/rE6i62q5vo+2gI5dJA=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
|
||||
|
||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
|
||||
|
||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw=="],
|
||||
|
||||
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.2.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pHVzDYsnaDmBlAuwim45y3soIN8H4R7KbkSVirGhXO+R/kO2OLCe0eucUEbddaTcdMHHdzcIGHtZSMSQlA+apw=="],
|
||||
|
||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw=="],
|
||||
|
||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.4", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA=="],
|
||||
|
||||
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw=="],
|
||||
|
||||
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="],
|
||||
|
||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="],
|
||||
|
||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
|
||||
|
||||
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.4", "@radix-ui/react-portal": "1.1.6", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-DyW8VVeeMSSLFvAmnVnCwvI3H+1tpJFHT50r+tdOoMse9XqYDBCcyux8u3G2y+LOpt7fPQ6KKH0mhs+ce1+Z5w=="],
|
||||
|
||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||
|
||||
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
|
||||
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
|
||||
|
||||
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
|
||||
|
||||
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
|
||||
|
||||
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
|
||||
|
||||
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.0", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-rQj0aAWOpCdCMRbI6pLQm8r7S2BM3YhTa0SzOYD55k+hJA8oo9J+H+9wLM9oMlZWOX/wJWPTzfDfmZkf7LvCfg=="],
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||
|
||||
"@react-aria/focus": ["@react-aria/focus@3.20.2", "", { "dependencies": { "@react-aria/interactions": "^3.25.0", "@react-aria/utils": "^3.28.2", "@react-types/shared": "^3.29.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-Q3rouk/rzoF/3TuH6FzoAIKrl+kzZi9LHmr8S5EqLAOyP9TXIKG34x2j42dZsAhrw7TbF9gA8tBKwnCNH4ZV+Q=="],
|
||||
|
||||
"@react-aria/interactions": ["@react-aria/interactions@3.25.0", "", { "dependencies": { "@react-aria/ssr": "^3.9.8", "@react-aria/utils": "^3.28.2", "@react-stately/flags": "^3.1.1", "@react-types/shared": "^3.29.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-GgIsDLlO8rDU/nFn6DfsbP9rfnzhm8QFjZkB9K9+r+MTSCn7bMntiWQgMM+5O6BiA8d7C7x4zuN4bZtc0RBdXQ=="],
|
||||
|
||||
"@react-aria/ssr": ["@react-aria/ssr@3.9.8", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw=="],
|
||||
|
||||
"@react-aria/utils": ["@react-aria/utils@3.28.2", "", { "dependencies": { "@react-aria/ssr": "^3.9.8", "@react-stately/flags": "^3.1.1", "@react-stately/utils": "^3.10.6", "@react-types/shared": "^3.29.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-J8CcLbvnQgiBn54eeEvQQbIOfBF3A1QizxMw9P4cl9MkeR03ug7RnjTIdJY/n2p7t59kLeAB3tqiczhcj+Oi5w=="],
|
||||
|
||||
"@react-stately/flags": ["@react-stately/flags@3.1.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg=="],
|
||||
|
||||
"@react-stately/utils": ["@react-stately/utils@3.10.6", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-O76ip4InfTTzAJrg8OaZxKU4vvjMDOpfA/PGNOytiXwBbkct2ZeZwaimJ8Bt9W1bj5VsZ81/o/tW4BacbdDOMA=="],
|
||||
|
||||
"@react-types/shared": ["@react-types/shared@3.29.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-IDQYu/AHgZimObzCFdNl1LpZvQW/xcfLt3v20sorl5qRucDVj4S9os98sVTZ4IRIBjmS+MkjqpR5E70xan7ooA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.0", "", { "os": "android", "cpu": "arm" }, "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.0", "", { "os": "android", "cpu": "arm64" }, "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.40.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.40.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="],
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.40.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.40.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="],
|
||||
|
||||
"@swc/core": ["@swc/core@1.11.22", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.21" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.11.22", "@swc/core-darwin-x64": "1.11.22", "@swc/core-linux-arm-gnueabihf": "1.11.22", "@swc/core-linux-arm64-gnu": "1.11.22", "@swc/core-linux-arm64-musl": "1.11.22", "@swc/core-linux-x64-gnu": "1.11.22", "@swc/core-linux-x64-musl": "1.11.22", "@swc/core-win32-arm64-msvc": "1.11.22", "@swc/core-win32-ia32-msvc": "1.11.22", "@swc/core-win32-x64-msvc": "1.11.22" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-mjPYbqq8XjwqSE0hEPT9CzaJDyxql97LgK4iyvYlwVSQhdN1uK0DBG4eP9PxYzCS2MUGAXB34WFLegdUj5HGpg=="],
|
||||
|
||||
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.11.22", "", { "os": "darwin", "cpu": "arm64" }, "sha512-upSiFQfo1TE2QM3+KpBcp5SrOdKKjoc+oUoD1mmBDU2Wv4Bjjv16Z2I5ADvIqMV+b87AhYW+4Qu6iVrQD7j96Q=="],
|
||||
|
||||
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.11.22", "", { "os": "darwin", "cpu": "x64" }, "sha512-8PEuF/gxIMJVK21DjuCOtzdqstn2DqnxVhpAYfXEtm3WmMqLIOIZBypF/xafAozyaHws4aB/5xmz8/7rPsjavw=="],
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.11.22", "", { "os": "linux", "cpu": "arm" }, "sha512-NIPTXvqtn9e7oQHgdaxM9Z/anHoXC3Fg4ZAgw5rSGa1OlnKKupt5sdfJamNggSi+eAtyoFcyfkgqHnfe2u63HA=="],
|
||||
|
||||
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.11.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-xZ+bgS60c5r8kAeYsLNjJJhhQNkXdidQ277pUabSlu5GjR0CkQUPQ+L9hFeHf8DITEqpPBPRiAiiJsWq5eqMBg=="],
|
||||
|
||||
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.11.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-JhrP/q5VqQl2eJR0xKYIkKTPjgf8CRsAmRnjJA2PtZhfQ543YbYvUqxyXSRyBOxdyX8JwzuAxIPEAlKlT7PPuQ=="],
|
||||
|
||||
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.11.22", "", { "os": "linux", "cpu": "x64" }, "sha512-htmAVL+U01gk9GyziVUP0UWYaUQBgrsiP7Ytf6uDffrySyn/FclUS3MDPocNydqYsOpj3OpNKPxkaHK+F+X5fg=="],
|
||||
|
||||
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.11.22", "", { "os": "linux", "cpu": "x64" }, "sha512-PL0VHbduWPX+ANoyOzr58jBiL2VnD0xGSFwPy7NRZ1Pr6SNWm4jw3x2u6RjLArGhS5EcWp64BSk9ZxqmTV3FEg=="],
|
||||
|
||||
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.11.22", "", { "os": "win32", "cpu": "arm64" }, "sha512-moJvFhhTVGoMeEThtdF7hQog80Q00CS06v5uB+32VRuv+I31+4WPRyGlTWHO+oY4rReNcXut/mlDHPH7p0LdFg=="],
|
||||
|
||||
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.11.22", "", { "os": "win32", "cpu": "ia32" }, "sha512-/jnsPJJz89F1aKHIb5ScHkwyzBciz2AjEq2m9tDvQdIdVufdJ4SpEDEN9FqsRNRLcBHjtbLs6bnboA+B+pRFXw=="],
|
||||
|
||||
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.11.22", "", { "os": "win32", "cpu": "x64" }, "sha512-lc93Y8Mku7LCFGqIxJ91coXZp2HeoDcFZSHCL90Wttg5xhk5xVM9uUCP+OdQsSsEixLF34h5DbT9ObzP8rAdRw=="],
|
||||
|
||||
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
||||
|
||||
"@swc/types": ["@swc/types@0.1.21", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ=="],
|
||||
|
||||
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.6", "", { "dependencies": { "@tanstack/virtual-core": "3.13.6" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA=="],
|
||||
|
||||
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.6", "", {}, "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="],
|
||||
|
||||
"@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="],
|
||||
|
||||
"@types/react": ["@types/react@18.3.20", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@18.3.6", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.31.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.31.0", "@typescript-eslint/type-utils": "8.31.0", "@typescript-eslint/utils": "8.31.0", "@typescript-eslint/visitor-keys": "8.31.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.31.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.31.0", "@typescript-eslint/types": "8.31.0", "@typescript-eslint/typescript-estree": "8.31.0", "@typescript-eslint/visitor-keys": "8.31.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.31.0", "", { "dependencies": { "@typescript-eslint/types": "8.31.0", "@typescript-eslint/visitor-keys": "8.31.0" } }, "sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.31.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.31.0", "@typescript-eslint/utils": "8.31.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.31.0", "", {}, "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.31.0", "", { "dependencies": { "@typescript-eslint/types": "8.31.0", "@typescript-eslint/visitor-keys": "8.31.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.31.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.31.0", "@typescript-eslint/types": "8.31.0", "@typescript-eslint/typescript-estree": "8.31.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.31.0", "", { "dependencies": { "@typescript-eslint/types": "8.31.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ=="],
|
||||
|
||||
"@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.9.0", "", { "dependencies": { "@swc/core": "^1.11.21" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q=="],
|
||||
|
||||
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001715", "", {}, "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="],
|
||||
|
||||
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
|
||||
|
||||
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.142", "", {}, "sha512-Ah2HgkTu/9RhTDNThBtzu2Wirdy4DC9b0sMT1pUhbkZQ5U/iwmE+PHZX1MpjD5IkJCc2wSghgGG/B04szAx07w=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.25.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.13.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.25.1", "@eslint/plugin-kit": "^0.2.8", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ=="],
|
||||
|
||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
|
||||
|
||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.20", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||
|
||||
"espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
|
||||
|
||||
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
|
||||
|
||||
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
|
||||
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.475.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
|
||||
|
||||
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
||||
|
||||
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="],
|
||||
|
||||
"postcss-js": ["postcss-js@4.0.1", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw=="],
|
||||
|
||||
"postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="],
|
||||
|
||||
"postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
|
||||
|
||||
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
|
||||
|
||||
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
|
||||
|
||||
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
|
||||
|
||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rollup": ["rollup@4.40.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.0", "@rollup/rollup-android-arm64": "4.40.0", "@rollup/rollup-darwin-arm64": "4.40.0", "@rollup/rollup-darwin-x64": "4.40.0", "@rollup/rollup-freebsd-arm64": "4.40.0", "@rollup/rollup-freebsd-x64": "4.40.0", "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", "@rollup/rollup-linux-arm-musleabihf": "4.40.0", "@rollup/rollup-linux-arm64-gnu": "4.40.0", "@rollup/rollup-linux-arm64-musl": "4.40.0", "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-musl": "4.40.0", "@rollup/rollup-linux-s390x-gnu": "4.40.0", "@rollup/rollup-linux-x64-gnu": "4.40.0", "@rollup/rollup-linux-x64-musl": "4.40.0", "@rollup/rollup-win32-arm64-msvc": "4.40.0", "@rollup/rollup-win32-ia32-msvc": "4.40.0", "@rollup/rollup-win32-x64-msvc": "4.40.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||
|
||||
"semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="],
|
||||
|
||||
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
|
||||
|
||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||
|
||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
||||
|
||||
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.31.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.31.0", "@typescript-eslint/parser": "8.31.0", "@typescript-eslint/utils": "8.31.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-u+93F0sB0An8WEAPtwxVhFby573E8ckdjwUUQUj9QA4v8JAvgtoDdIyYR3XFwFHq2W1KJ1AurwJCO+w+Y1ixyQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"vite": ["vite@5.4.18", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
}
|
||||
}
|
||||
BIN
web/bun.lockb
Executable file
272
web/index.html
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
{% if theme == 'neutral' %}
|
||||
<meta
|
||||
name="theme-color"
|
||||
@@ -27,9 +26,9 @@
|
||||
content="#030712"
|
||||
>
|
||||
{% endif %}
|
||||
<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">
|
||||
<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">
|
||||
<title>Cup</title>
|
||||
</head>
|
||||
<body>
|
||||
@@ -226,97 +225,182 @@
|
||||
</div>
|
||||
<ul>
|
||||
{% for server in server_ids %}
|
||||
<li class="mb-4 last:mb-0">
|
||||
<p
|
||||
class="my-4 text-lg font-semibold text-{{ theme }}-600 dark:text-{{ theme }}-400 px-6"
|
||||
>
|
||||
{% if server == '' %}
|
||||
Local images
|
||||
{% else %}
|
||||
{{ server }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<ul
|
||||
class="dark:divide-{{theme}}-900 divide-y dark:text-white"
|
||||
>
|
||||
{% for image in servers[server] %}
|
||||
<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"
|
||||
{% if server == '' %}
|
||||
<li class="mb-8 last:mb-0">
|
||||
<ul
|
||||
class="dark:divide-{{theme}}-900 divide-y dark:text-white"
|
||||
>
|
||||
{% for image in servers[server] %}
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
<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 }}
|
||||
</p>
|
||||
<ul
|
||||
class="dark:divide-{{ theme }}-900 divide-y dark:text-white"
|
||||
>
|
||||
{% for image in servers[server] %}
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
{{ image.name }}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.2.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"caniuse-lite": "^1.0.30001698",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
@@ -24,6 +24,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",
|
||||
@@ -40,4 +41,4 @@
|
||||
"typescript-eslint": "^8.0.1",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,11 @@ import Statistic from "./components/Statistic";
|
||||
import Image from "./components/Image";
|
||||
import { LastChecked } from "./components/LastChecked";
|
||||
import Loading from "./components/Loading";
|
||||
import { Filters as FiltersType } from "./types";
|
||||
import { Data } from "./types";
|
||||
import { theme } from "./theme";
|
||||
import RefreshButton from "./components/RefreshButton";
|
||||
import Search from "./components/Search";
|
||||
import { Server } from "./components/Server";
|
||||
import { useData } from "./hooks/use-data";
|
||||
import DataLoadingError from "./components/DataLoadingError";
|
||||
import Filters from "./components/Filters";
|
||||
import { Filter, FilterX } from "lucide-react";
|
||||
import { WithTooltip } from "./components/ui/Tooltip";
|
||||
import { getDescription } from "./utils";
|
||||
|
||||
const SORT_ORDER = [
|
||||
"monitored_images",
|
||||
@@ -28,25 +22,9 @@ const SORT_ORDER = [
|
||||
];
|
||||
|
||||
function App() {
|
||||
const { data, isLoading, isError } = useData();
|
||||
|
||||
const [showFilters, setShowFilters] = useState<boolean>(false);
|
||||
const [filters, setFilters] = useState<FiltersType>({
|
||||
onlyInUse: false,
|
||||
registries: [],
|
||||
statuses: [],
|
||||
});
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
if (isLoading) return <Loading />;
|
||||
if (isError || !data) return <DataLoadingError />;
|
||||
const toggleShowFilters = () => {
|
||||
if (showFilters) {
|
||||
setFilters({ onlyInUse: false, registries: [], statuses: [] });
|
||||
}
|
||||
setShowFilters(!showFilters);
|
||||
};
|
||||
|
||||
if (!data) return <Loading onLoad={setData} />;
|
||||
return (
|
||||
<div
|
||||
className={`flex min-h-screen justify-center bg-white dark:bg-${theme}-950`}
|
||||
@@ -80,38 +58,18 @@ function App() {
|
||||
className={`border shadow-sm border-${theme}-200 dark:border-${theme}-900 my-8 rounded-md`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center justify-between gap-3 px-6 py-4 text-${theme}-500`}
|
||||
className={`flex items-center justify-between px-6 py-4 text-${theme}-500`}
|
||||
>
|
||||
<LastChecked datetime={data.last_updated} />
|
||||
<div className="flex gap-3">
|
||||
<WithTooltip
|
||||
text={showFilters ? "Clear filters" : "Show filters"}
|
||||
>
|
||||
<button onClick={toggleShowFilters}>
|
||||
{showFilters ? <FilterX /> : <Filter />}
|
||||
</button>
|
||||
</WithTooltip>
|
||||
<RefreshButton />
|
||||
</div>
|
||||
<RefreshButton />
|
||||
</div>
|
||||
<div className="flex gap-2 px-6 text-black dark:text-white">
|
||||
<Search onChange={setSearchQuery} />
|
||||
</div>
|
||||
{showFilters && (
|
||||
<Filters
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
registries={[
|
||||
...new Set(data.images.map((image) => image.parts.registry)),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Search onChange={setSearchQuery} />
|
||||
<ul>
|
||||
{Object.entries(
|
||||
data.images.reduce<Record<string, typeof data.images>>(
|
||||
(acc, image) => {
|
||||
const server = image.server ?? "";
|
||||
if (!Object.hasOwn(acc, server)) acc[server] = [];
|
||||
if (!acc[server]) acc[server] = [];
|
||||
acc[server].push(image);
|
||||
return acc;
|
||||
},
|
||||
@@ -122,19 +80,6 @@ function App() {
|
||||
.map(([server, images]) => (
|
||||
<Server name={server} key={server}>
|
||||
{images
|
||||
.filter((image) =>
|
||||
filters.onlyInUse ? !!image.in_use : true,
|
||||
)
|
||||
.filter((image) =>
|
||||
filters.registries.length == 0
|
||||
? true
|
||||
: filters.registries.includes(image.parts.registry),
|
||||
)
|
||||
.filter((image) =>
|
||||
filters.statuses.length == 0
|
||||
? true
|
||||
: filters.statuses.includes(getDescription(image)),
|
||||
)
|
||||
.filter((image) => image.reference.includes(searchQuery))
|
||||
.map((image) => (
|
||||
<Image data={image} key={image.reference} />
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { theme } from "../theme";
|
||||
|
||||
export default function Badge({ from, to }: { from: string; to: string }) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full bg-${theme}-50 px-2 py-1 text-xs font-medium text-${theme}-700 ring-1 ring-inset ring-${theme}-700/10 dark:bg-${theme}-400/10 dark:text-${theme}-400 dark:ring-${theme}-400/30 break-keep`}
|
||||
>
|
||||
{from}
|
||||
<ArrowRight className="size-3" />
|
||||
{to}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -21,8 +21,6 @@ export function CodeBlock({
|
||||
};
|
||||
};
|
||||
|
||||
const copyText = children instanceof Array ? children.join("") : children;
|
||||
|
||||
return (
|
||||
<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`}
|
||||
@@ -37,7 +35,7 @@ export function CodeBlock({
|
||||
) : (
|
||||
<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`}
|
||||
onClick={handleCopy(`${copyText}`)}
|
||||
onClick={handleCopy(`docker pull ${children}`)}
|
||||
>
|
||||
<Clipboard className="size-5" />
|
||||
</button>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import Logo from "./Logo";
|
||||
import { theme } from "../theme";
|
||||
|
||||
const DataLoadingError = () => {
|
||||
return (
|
||||
<div
|
||||
className={`flex min-h-screen justify-center bg-${theme}-50 dark:bg-${theme}-950`}
|
||||
>
|
||||
<div className="absolute mx-auto h-full w-full max-w-[80rem] overflow-hidden px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto my-8 flex h-full max-w-[48rem] flex-col">
|
||||
<div className="flex items-center gap-1">
|
||||
<h1 className="text-5xl font-bold lg:text-6xl dark:text-white">
|
||||
Cup
|
||||
</h1>
|
||||
<Logo />
|
||||
</div>
|
||||
<div
|
||||
className={`flex h-full flex-col items-center justify-center gap-1 text-${theme}-500 dark:text-${theme}-400`}
|
||||
>
|
||||
<div className="mb-8 flex gap-1">
|
||||
An error occurred, please try again.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataLoadingError;
|
||||
@@ -1,81 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { theme } from "../theme";
|
||||
import { Filters as FiltersType } from "../types";
|
||||
import { Checkbox } from "./ui/Checkbox";
|
||||
import Select from "./ui/Select";
|
||||
import { Server } from "lucide-react";
|
||||
|
||||
interface Props {
|
||||
filters: FiltersType;
|
||||
setFilters: (filters: FiltersType) => void;
|
||||
registries: string[];
|
||||
}
|
||||
|
||||
const STATUSES = [
|
||||
"Major update",
|
||||
"Minor update",
|
||||
"Patch update",
|
||||
"Digest update",
|
||||
"Up to date",
|
||||
"Unknown",
|
||||
];
|
||||
|
||||
export default function Filters({ filters, setFilters, registries }: Props) {
|
||||
const [selectedRegistries, setSelectedRegistries] = useState<
|
||||
FiltersType["registries"]
|
||||
>([]);
|
||||
const [selectedStatuses, setSelectedStatuses] = useState<
|
||||
FiltersType["statuses"]
|
||||
>([]);
|
||||
const handleSelectRegistries = (registries: string[]) => {
|
||||
setSelectedRegistries(registries);
|
||||
setFilters({
|
||||
...filters,
|
||||
registries,
|
||||
});
|
||||
};
|
||||
const handleSelectStatuses = (statuses: string[]) => {
|
||||
if (statuses.every((status) => STATUSES.includes(status))) {
|
||||
setSelectedStatuses(statuses as FiltersType["statuses"]);
|
||||
setFilters({
|
||||
...filters,
|
||||
statuses: statuses as FiltersType["statuses"],
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-4 px-6 py-4 sm:flex-row">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="inUse"
|
||||
checked={filters.onlyInUse}
|
||||
onCheckedChange={(value) => {
|
||||
setFilters({
|
||||
...filters,
|
||||
onlyInUse: value === "indeterminate" ? false : value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor="inUse"
|
||||
className={`text-sm font-medium leading-none text-${theme}-600 dark:text-${theme}-400 transition-colors duration-200 hover:text-black peer-hover:text-black peer-data-[state=checked]:text-black dark:hover:text-white peer-hover:dark:text-white dark:peer-data-[state=checked]:text-white`}
|
||||
>
|
||||
Hide unused images
|
||||
</label>
|
||||
</div>
|
||||
<Select
|
||||
Icon={Server}
|
||||
items={registries}
|
||||
placeholder="Registry"
|
||||
selectedItems={selectedRegistries}
|
||||
setSelectedItems={handleSelectRegistries}
|
||||
/>
|
||||
<Select
|
||||
items={STATUSES}
|
||||
placeholder="Update type"
|
||||
selectedItems={selectedStatuses}
|
||||
setSelectedItems={handleSelectStatuses}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
DialogPanel,
|
||||
DialogTitle,
|
||||
} from "@headlessui/react";
|
||||
import { WithTooltip } from "./ui/Tooltip";
|
||||
import { WithTooltip } from "./Tooltip";
|
||||
import type { Image } from "../types";
|
||||
import { theme } from "../theme";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
TriangleAlert,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import Badge from "./Badge";
|
||||
import { getDescription } from "../utils";
|
||||
|
||||
const clickable_registries = [
|
||||
"registry-1.docker.io",
|
||||
@@ -40,11 +38,8 @@ export default function Image({ data }: { data: Image }) {
|
||||
data.result.info?.type == "version"
|
||||
? data.reference.split(":")[0] + ":" + data.result.info.new_tag
|
||||
: data.reference;
|
||||
const info = getInfo(data);
|
||||
let url: string | null = null;
|
||||
if (data.url) {
|
||||
url = data.url;
|
||||
} else if (clickable_registries.includes(data.parts.registry)) {
|
||||
if (clickable_registries.includes(data.parts.registry)) {
|
||||
switch (data.parts.registry) {
|
||||
case "registry-1.docker.io":
|
||||
url = `https://hub.docker.com/r/${data.parts.repository}`;
|
||||
@@ -62,20 +57,7 @@ export default function Image({ data }: { data: Image }) {
|
||||
>
|
||||
<Box className={`size-6 shrink-0 text-${theme}-500`} />
|
||||
<span className="font-mono">{data.reference}</span>
|
||||
<div className="ml-auto flex gap-2">
|
||||
{data.result.info?.type === "version" ? (
|
||||
<Badge
|
||||
from={data.result.info.current_version}
|
||||
to={data.result.info.new_version}
|
||||
/>
|
||||
) : null}
|
||||
<WithTooltip
|
||||
text={info.description}
|
||||
className={`size-6 shrink-0 ${info.color}`}
|
||||
>
|
||||
<info.icon />
|
||||
</WithTooltip>
|
||||
</div>
|
||||
<Icon data={data} />
|
||||
</li>
|
||||
</button>
|
||||
<Dialog open={open} onClose={setOpen} className="relative z-10">
|
||||
@@ -94,14 +76,14 @@ export default function Image({ data }: { data: Image }) {
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<Box className={`size-6 shrink-0 text-${theme}-500`} />
|
||||
<DialogTitle className="break-all font-mono text-black dark:text-white">
|
||||
<DialogTitle className="font-mono text-black dark:text-white">
|
||||
{url ? (
|
||||
<>
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`group w-fit hover:underline`}
|
||||
className={`group w-fit text-black hover:underline dark:text-white`}
|
||||
>
|
||||
<span>
|
||||
{data.reference}
|
||||
@@ -131,8 +113,7 @@ export default function Image({ data }: { data: Image }) {
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<info.icon className={`size-6 shrink-0 ${info.color}`} />
|
||||
{info.description}
|
||||
<DialogIcon data={data} />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Timer className="size-6 shrink-0 text-gray-500" />
|
||||
@@ -183,49 +164,118 @@ export default function Image({ data }: { data: Image }) {
|
||||
);
|
||||
}
|
||||
|
||||
function getInfo(data: Image): {
|
||||
color: string;
|
||||
icon: typeof HelpCircle;
|
||||
description: string;
|
||||
} {
|
||||
const description = getDescription(data);
|
||||
switch (description) {
|
||||
case "Unknown":
|
||||
return {
|
||||
color: "text-gray-500",
|
||||
icon: HelpCircle,
|
||||
description,
|
||||
};
|
||||
case "Up to date":
|
||||
return {
|
||||
color: "text-green-500",
|
||||
icon: CircleCheck,
|
||||
description,
|
||||
};
|
||||
|
||||
case "Major update":
|
||||
return {
|
||||
color: "text-red-500",
|
||||
icon: CircleArrowUp,
|
||||
description,
|
||||
};
|
||||
case "Minor update":
|
||||
return {
|
||||
color: "text-yellow-500",
|
||||
icon: CircleArrowUp,
|
||||
description,
|
||||
};
|
||||
case "Patch update":
|
||||
return {
|
||||
color: "text-blue-500",
|
||||
icon: CircleArrowUp,
|
||||
description,
|
||||
};
|
||||
case "Digest update":
|
||||
return {
|
||||
color: "text-blue-500",
|
||||
icon: CircleArrowUp,
|
||||
description,
|
||||
};
|
||||
function Icon({ data }: { data: Image }) {
|
||||
switch (data.result.has_update) {
|
||||
case null:
|
||||
return (
|
||||
<WithTooltip
|
||||
text="Unknown"
|
||||
className="ml-auto size-6 shrink-0 text-gray-500"
|
||||
>
|
||||
<HelpCircle />
|
||||
</WithTooltip>
|
||||
);
|
||||
case false:
|
||||
return (
|
||||
<WithTooltip
|
||||
text="Up to date"
|
||||
className="ml-auto size-6 shrink-0 text-green-500"
|
||||
>
|
||||
<CircleCheck />
|
||||
</WithTooltip>
|
||||
);
|
||||
case true:
|
||||
if (data.result.info?.type === "version") {
|
||||
switch (data.result.info.version_update_type) {
|
||||
case "major":
|
||||
return (
|
||||
<WithTooltip
|
||||
text="Major Update"
|
||||
className="ml-auto size-6 shrink-0 text-red-500"
|
||||
>
|
||||
<CircleArrowUp />
|
||||
</WithTooltip>
|
||||
);
|
||||
case "minor":
|
||||
return (
|
||||
<WithTooltip
|
||||
text="Minor Update"
|
||||
className="ml-auto size-6 shrink-0 text-yellow-500"
|
||||
>
|
||||
<CircleArrowUp />
|
||||
</WithTooltip>
|
||||
);
|
||||
case "patch":
|
||||
return (
|
||||
<WithTooltip
|
||||
text="Patch Update"
|
||||
className="ml-auto size-6 shrink-0 text-blue-500"
|
||||
>
|
||||
<CircleArrowUp />
|
||||
</WithTooltip>
|
||||
);
|
||||
}
|
||||
} else if (data.result.info?.type === "digest") {
|
||||
return (
|
||||
<WithTooltip
|
||||
text="Update available"
|
||||
className="ml-auto size-6 shrink-0 text-blue-500"
|
||||
>
|
||||
<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
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { Data } from "../types";
|
||||
import Logo from "./Logo";
|
||||
import { theme } from "../theme";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
|
||||
export default function Loading() {
|
||||
export default function Loading({ onLoad }: { onLoad: (data: Data) => void }) {
|
||||
fetch(
|
||||
process.env.NODE_ENV === "production"
|
||||
? "/api/v3/json"
|
||||
: `http://${window.location.hostname}:8000/api/v3/json`,
|
||||
).then((response) =>
|
||||
response.json().then((data) => {
|
||||
onLoad(data as Data);
|
||||
}),
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={`flex min-h-screen justify-center bg-${theme}-50 dark:bg-${theme}-950`}
|
||||
@@ -16,16 +26,9 @@ export default function Loading() {
|
||||
<Logo />
|
||||
</div>
|
||||
<div
|
||||
className={`flex h-full flex-col items-center justify-center gap-1 text-${theme}-500 dark:text-${theme}-400`}
|
||||
className={`flex h-full items-center justify-center gap-1 text-${theme}-500 dark:text-${theme}-400`}
|
||||
>
|
||||
<div className="mb-8 flex gap-1">
|
||||
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>
|
||||
Loading <RefreshCw className="animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { WithTooltip } from "./ui/Tooltip";
|
||||
import { WithTooltip } from "./Tooltip";
|
||||
|
||||
export default function RefreshButton() {
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
@@ -14,14 +14,14 @@ export default function RefreshButton() {
|
||||
request.open(
|
||||
"GET",
|
||||
process.env.NODE_ENV === "production"
|
||||
? "./api/v3/refresh"
|
||||
? "/api/v3/refresh"
|
||||
: `http://${window.location.hostname}:8000/api/v3/refresh`,
|
||||
);
|
||||
request.send();
|
||||
};
|
||||
return (
|
||||
<WithTooltip text="Reload">
|
||||
<button className="group shrink-0" onClick={refresh} disabled={disabled}>
|
||||
<button className="group" onClick={refresh} disabled={disabled}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
@@ -32,7 +32,7 @@ export default function RefreshButton() {
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="size-6 group-disabled:animate-spin"
|
||||
className="shrink-0 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" />
|
||||
|
||||
@@ -23,30 +23,30 @@ export default function Search({
|
||||
onChange("");
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={`flex w-full items-center rounded-md border border-${theme}-200 dark:border-${theme}-700 gap-1 px-2 bg-${theme}-100 dark:bg-${theme}-900 group relative flex-nowrap`}
|
||||
>
|
||||
<SearchIcon
|
||||
className={`size-5 text-${theme}-600 dark:text-${theme}-400`}
|
||||
/>
|
||||
<div className="w-full">
|
||||
<input
|
||||
className={`h-10 w-full text-sm text-${theme}-800 dark:text-${theme}-200 peer bg-transparent focus:outline-none placeholder:text-${theme}-600 placeholder:dark:text-${theme}-400`}
|
||||
placeholder="Search"
|
||||
onChange={handleChange}
|
||||
value={searchQuery}
|
||||
></input>
|
||||
</div>
|
||||
{showClear && (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className={`hover:text-${theme}-600 dark:hover:text-${theme}-400 transition-colors duration-200`}
|
||||
>
|
||||
<X className="size-5" />
|
||||
</button>
|
||||
)}
|
||||
<div className={`w-full px-6 text-black dark:text-white`}>
|
||||
<div
|
||||
className="absolute -bottom-px left-1/2 h-full w-0 -translate-x-1/2 rounded-md border-b-2 border-b-blue-600 transition-all duration-200 group-has-[:focus]:w-[calc(100%+2px)]"
|
||||
className={`flex w-full items-center rounded-md border border-${theme}-200 dark:border-${theme}-700 gap-1 px-2 bg-${theme}-100 dark:bg-${theme}-900 peer flex-nowrap`}
|
||||
>
|
||||
<SearchIcon className={`size-5 text-${theme}-600 dark:text-${theme}-400`} />
|
||||
<div className="w-full">
|
||||
<input
|
||||
className={`h-10 w-full text-sm text-${theme}-800 dark:text-${theme}-200 peer bg-transparent focus:outline-none placeholder:text-${theme}-600 placeholder:dark:text-${theme}-400`}
|
||||
placeholder="Search"
|
||||
onChange={handleChange}
|
||||
value={searchQuery}
|
||||
></input>
|
||||
</div>
|
||||
{showClear && (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className={`hover:text-${theme}-600 dark:hover:text-${theme}-400 transition-colors duration-200`}
|
||||
>
|
||||
<X className="size-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="relative left-1/2 h-[8px] w-0 -translate-x-1/2 -translate-y-[8px] rounded-md border-b-2 border-b-blue-600 transition-all duration-200 peer-has-[:focus]:w-full"
|
||||
style={{ clipPath: "inset(calc(100% - 2px) 0 0 0)" }}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Provider, Root, Trigger, Content } from "@radix-ui/react-tooltip";
|
||||
import { cn } from "../../utils";
|
||||
import { cn } from "../utils";
|
||||
import { forwardRef, ReactNode } from "react";
|
||||
import { theme } from "../../theme";
|
||||
import { theme } from "../theme";
|
||||
|
||||
const TooltipContent = forwardRef<
|
||||
React.ElementRef<typeof Content>,
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
import { cn } from "../../utils";
|
||||
import { theme } from "../../theme";
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
`border-${theme}-600 dark:border-${theme}-400 group peer h-4 w-4 shrink-0 rounded-sm border shadow transition-colors duration-200 hover:border-black focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-blue-500 data-[state=checked]:border-0 data-[state=checked]:bg-blue-500 data-[state=checked]:text-white hover:data-[state=checked]:bg-blue-600 dark:hover:border-white dark:hover:data-[state=checked]:bg-blue-400`,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check
|
||||
className={`h-3 w-3 group-data-[state=checked]:text-white dark:group-data-[state=checked]:text-${theme}-950`}
|
||||
strokeWidth={3}
|
||||
/>
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
||||
@@ -1,84 +0,0 @@
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
} from "@headlessui/react";
|
||||
import { ChevronDown, Check } from "lucide-react";
|
||||
import { theme } from "../../theme";
|
||||
import { cn } from "../../utils";
|
||||
import { Server } from "lucide-react";
|
||||
|
||||
export default function Select({
|
||||
items,
|
||||
Icon,
|
||||
placeholder,
|
||||
selectedItems,
|
||||
setSelectedItems,
|
||||
}: {
|
||||
items: string[];
|
||||
Icon?: typeof Server;
|
||||
placeholder: string;
|
||||
selectedItems: string[];
|
||||
setSelectedItems: (items: string[]) => void;
|
||||
}) {
|
||||
return (
|
||||
<Listbox value={selectedItems} onChange={setSelectedItems} multiple>
|
||||
<div className="relative">
|
||||
<ListboxButton
|
||||
className={cn(
|
||||
`flex overflow-x-hidden w-full gap-2 rounded-md bg-${theme}-100 dark:bg-${theme}-900 border border-${theme}-200 dark:border-${theme}-700 group relative items-center py-1.5 pl-3 pr-2 text-left transition-colors duration-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-blue-500 sm:text-sm/6`,
|
||||
selectedItems.length == 0
|
||||
? `text-${theme}-600 dark:text-${theme}-400 hover:text-black hover:dark:text-white`
|
||||
: "text-black dark:text-white",
|
||||
)}
|
||||
>
|
||||
{Icon && (
|
||||
<Icon
|
||||
className={cn(
|
||||
"size-4 shrink-0",
|
||||
selectedItems.length == 0
|
||||
? `text-${theme}-600 dark:text-${theme}-400 hover:text-black hover:dark:text-white`
|
||||
: "text-black dark:text-white",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span className="truncate">
|
||||
{selectedItems.length == 0
|
||||
? placeholder
|
||||
: selectedItems.length == 1
|
||||
? selectedItems[0]
|
||||
: `${selectedItems[0]} +${(selectedItems.length - 1).toString()} more`}</span>
|
||||
|
||||
<ChevronDown
|
||||
aria-hidden="true"
|
||||
className={`size-5 shrink-0 ml-auto self-center text-${theme}-600 dark:text-${theme}-400 transition-colors duration-200 group-hover:text-black sm:size-4 group-hover:dark:text-white`}
|
||||
/>
|
||||
<div
|
||||
className="absolute -bottom-px left-1/2 h-full w-0 -translate-x-1/2 rounded-md border-b-2 border-b-blue-600 transition-all duration-200 group-data-[open]:w-[calc(100%+2px)]"
|
||||
style={{ clipPath: "inset(calc(100% - 2px) 0 0 0)" }}
|
||||
></div>
|
||||
</ListboxButton>
|
||||
<ListboxOptions
|
||||
transition
|
||||
className={`absolute z-10 mt-1 max-h-56 w-max overflow-y-auto overflow-x-hidden rounded-md bg-${theme}-100 dark:bg-${theme}-900 border border-${theme}-200 dark:border-${theme}-700 text-base shadow-lg ring-1 ring-black/5 focus:outline-none data-[closed]:data-[leave]:opacity-0 data-[leave]:transition data-[leave]:duration-100 data-[leave]:ease-in sm:text-sm`}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<ListboxOption
|
||||
key={item}
|
||||
value={item}
|
||||
className={`group relative cursor-pointer text-nowrap py-2 pl-3 pr-9 data-[focus]:outline-none text-${theme}-600 dark:text-${theme}-400 transition-colors duration-200 data-[focus]:bg-black/10 data-[focus]:text-black dark:data-[focus]:bg-white/10 data-[focus]:dark:text-white`}
|
||||
>
|
||||
{item}
|
||||
<span
|
||||
className={`absolute inset-y-0 right-2 flex items-center text-${theme}-600 dark:text-${theme}-400 group-[:not([data-selected])]:hidden group-data-[focus]:text-black group-data-[focus]:dark:text-white`}
|
||||
>
|
||||
<Check aria-hidden="true" className="size-4" />
|
||||
</span>
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
</div>
|
||||
</Listbox>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Data } from "../types";
|
||||
|
||||
export const useData = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isError || !!data) return;
|
||||
setIsLoading(true);
|
||||
setIsError(false);
|
||||
setData(null);
|
||||
fetch(
|
||||
process.env.NODE_ENV === "production"
|
||||
? "./api/v3/json"
|
||||
: `http://${window.location.hostname}:8000/api/v3/json`,
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.ok) return response.json();
|
||||
throw new Error("Failed to fetch data");
|
||||
})
|
||||
.then((data) => {
|
||||
setData(data as Data);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
setIsError(true);
|
||||
console.error(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [data, isError, isLoading]);
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
};
|
||||
};
|
||||
@@ -20,7 +20,6 @@ export interface Image {
|
||||
repository: string;
|
||||
tag: string;
|
||||
};
|
||||
url: string | null;
|
||||
result: {
|
||||
has_update: boolean | null;
|
||||
info: VersionInfo | DigestInfo | null;
|
||||
@@ -28,7 +27,6 @@ export interface Image {
|
||||
};
|
||||
time: number;
|
||||
server: string | null;
|
||||
in_use: boolean | null;
|
||||
}
|
||||
|
||||
interface VersionInfo {
|
||||
@@ -44,16 +42,3 @@ interface DigestInfo {
|
||||
local_digests: string[];
|
||||
remote_digest: string;
|
||||
}
|
||||
|
||||
export interface Filters {
|
||||
onlyInUse: boolean;
|
||||
registries: string[];
|
||||
statuses: (
|
||||
| "Major update"
|
||||
| "Minor update"
|
||||
| "Patch update"
|
||||
| "Digest update"
|
||||
| "Up to date"
|
||||
| "Unknown"
|
||||
)[];
|
||||
}
|
||||
|
||||
@@ -1,30 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type { Image } from "./types";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function getDescription(image: Image) {
|
||||
switch (image.result.has_update) {
|
||||
case null:
|
||||
return "Unknown";
|
||||
case false:
|
||||
return "Up to date";
|
||||
case true:
|
||||
if (image.result.info?.type === "version") {
|
||||
switch (image.result.info.version_update_type) {
|
||||
case "major":
|
||||
return "Major update";
|
||||
case "minor":
|
||||
return "Minor update";
|
||||
case "patch":
|
||||
return "Patch update";
|
||||
}
|
||||
} else if (image.result.info?.type === "digest") {
|
||||
return "Digest update";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./src/App.tsx", "./src/components/**/*.tsx", "./index.liquid"],
|
||||
content: ["./src/App.tsx", "./src/components/*.tsx", "./index.liquid"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
variants: ["hover"],
|
||||
},
|
||||
{
|
||||
pattern: /bg-(gray|neutral)-(400|900|950)/,
|
||||
pattern: /bg-(gray|neutral)-(900|950)/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
{
|
||||
@@ -27,23 +27,27 @@ export default {
|
||||
variants: ["before:dark", "after:dark", "dark", "hover:dark"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-(50|300|200|400)/,
|
||||
pattern: /text-(gray|neutral)-(50|300|200)/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-600/,
|
||||
variants: ["*", "dark", "hover", "placeholder", "data-[placeholder]"],
|
||||
variants: ["dark", "hover"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-400/,
|
||||
variants: ["*:dark", "dark", "dark:hover", "placeholder:dark", "data-[placeholder]:dark"],
|
||||
variants: ["dark", "dark:hover"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-(500|700)/,
|
||||
pattern: /text-(gray|neutral)-600/,
|
||||
variants: ["placeholder"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-950/,
|
||||
variants: ["dark:group-data-[state=checked]"]
|
||||
pattern: /text-(gray|neutral)-400/,
|
||||
variants: ["placeholder:dark"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-700/,
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-800/,
|
||||
@@ -58,17 +62,10 @@ export default {
|
||||
variants: ["dark"],
|
||||
},
|
||||
{
|
||||
pattern: /border-(gray|neutral)-(600|300|400)/,
|
||||
pattern: /border-(gray|neutral)-(200|300)/,
|
||||
},
|
||||
{
|
||||
pattern: /border-(gray|neutral)-(400|700|800|900)/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
{
|
||||
pattern: /ring-(gray|neutral)-700/,
|
||||
},
|
||||
{
|
||||
pattern: /ring-(gray|neutral)-400/,
|
||||
pattern: /border-(gray|neutral)-(700|800|900)/,
|
||||
variants: ["dark"],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -4,7 +4,6 @@ import react from "@vitejs/plugin-react-swc";
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: "./",
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// https://stackoverflow.com/q/69614671/vite-without-hash-in-filename#75344943
|
||||
|
||||