From 139f96f938e7585fd0624d2c1c93bd54876e99c6 Mon Sep 17 00:00:00 2001 From: Joe Monk Date: Sun, 27 Apr 2025 02:22:08 +0100 Subject: [PATCH] Add a load of fallbacks and rework the docker bits --- biome.jsonc | 14 +++- package.json | 92 ++++++++++++------------- src/app/api/trpc/[trpc]/route.ts | 4 +- src/app/layout.tsx | 4 +- src/app/page.tsx | 33 +++------ src/server/api/routers/docker.ts | 113 +++++++++++++++++++++---------- src/server/api/trpc.ts | 3 +- src/styles/globals.css | 3 +- src/trpc/query-client.ts | 9 +-- src/trpc/react.tsx | 4 +- src/trpc/server.ts | 5 +- tsconfig.json | 9 +-- 12 files changed, 153 insertions(+), 140 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index a522e61..be63cdb 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -5,9 +5,17 @@ "clientKind": "git", "useIgnoreFile": true }, - "files": { "ignoreUnknown": false, "ignore": [] }, - "formatter": { "enabled": true }, - "organizeImports": { "enabled": true }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "lineWidth": 200 + }, + "organizeImports": { + "enabled": true + }, "linter": { "enabled": true, "rules": { diff --git a/package.json b/package.json index abfb6ff..d3df174 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,47 @@ { - "name": "dvdash", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "build": "next build", - "check": "biome check .", - "check:unsafe": "biome check --write --unsafe .", - "check:write": "biome check --write .", - "dev": "next dev --turbo", - "preview": "next build && next start", - "start": "next start", - "typecheck": "tsc --noEmit", - "update-regctl": "mkdir bin && curl -L https://github.com/regclient/regclient/releases/latest/download/regctl-linux-amd64 >bin/regctl && chmod 755 bin/regctl" - }, - "dependencies": { - "@t3-oss/env-nextjs": "^0.13.0", - "@tailwindcss/postcss": "^4.1.4", - "@tanstack/react-query": "^5.74.4", - "@trpc/client": "^11.1.1", - "@trpc/react-query": "^11.1.1", - "@trpc/server": "^11.1.1", - "daisyui": "^5.0.28", - "dockerode": "^4.0.6", - "next": "^15.3.1", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "semver": "^7.7.1", - "server-only": "^0.0.1", - "superjson": "^2.2.2", - "zod": "^3.24.3", - "zx": "^8.5.3", - "@biomejs/biome": "1.9.4", - "@types/bun": "^1.2.10", - "@types/dockerode": "^3.3.38", - "@types/react": "19.1.2", - "@types/react-dom": "^19.1.2", - "@types/semver": "^7.7.0", - "postcss": "^8.5.3", - "tailwindcss": "^4.1.4", - "typescript": "^5.8.3" - }, - "ct3aMetadata": { - "initVersion": "7.39.3" - } -} \ No newline at end of file + "name": "dvdash", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "next build", + "check": "biome check .", + "check:unsafe": "biome check --write --unsafe .", + "check:write": "biome check --write .", + "dev": "next dev --turbo", + "preview": "next build && next start", + "start": "next start", + "typecheck": "tsc --noEmit", + "update-regctl": "mkdir bin && curl -L https://github.com/regclient/regclient/releases/latest/download/regctl-linux-amd64 >bin/regctl && chmod 755 bin/regctl" + }, + "dependencies": { + "@t3-oss/env-nextjs": "^0.13.0", + "@tailwindcss/postcss": "^4.1.4", + "@tanstack/react-query": "^5.74.4", + "@trpc/client": "^11.1.1", + "@trpc/react-query": "^11.1.1", + "@trpc/server": "^11.1.1", + "daisyui": "^5.0.28", + "dockerode": "^4.0.6", + "next": "^15.3.1", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "semver": "^7.7.1", + "server-only": "^0.0.1", + "superjson": "^2.2.2", + "zod": "^3.24.3", + "zx": "^8.5.3", + "@biomejs/biome": "1.9.4", + "@types/bun": "^1.2.10", + "@types/dockerode": "^3.3.38", + "@types/react": "19.1.2", + "@types/react-dom": "^19.1.2", + "@types/semver": "^7.7.0", + "postcss": "^8.5.3", + "tailwindcss": "^4.1.4", + "typescript": "^5.8.3" + }, + "ct3aMetadata": { + "initVersion": "7.39.3" + } +} diff --git a/src/app/api/trpc/[trpc]/route.ts b/src/app/api/trpc/[trpc]/route.ts index d147e31..ae4d412 100644 --- a/src/app/api/trpc/[trpc]/route.ts +++ b/src/app/api/trpc/[trpc]/route.ts @@ -24,9 +24,7 @@ const handler = (req: NextRequest) => onError: env.NODE_ENV === "development" ? ({ path, error }) => { - console.error( - `❌ tRPC failed on ${path ?? ""}: ${error.message}`, - ); + console.error(`❌ tRPC failed on ${path ?? ""}: ${error.message}`); } : undefined, }); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d71f55e..18d5759 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -16,9 +16,7 @@ const geist = Geist({ variable: "--font-geist-sans", }); -export default function RootLayout({ - children, -}: Readonly<{ children: React.ReactNode }>) { +export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( diff --git a/src/app/page.tsx b/src/app/page.tsx index e5d587c..a5f91eb 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -16,45 +16,34 @@ export default async function Home() { Name Image - Version + Tag Short Hash - Version + Tag Short Hash {list .sort((ca, cb) => { - if (ca.Names[0] && cb.Names[0]) { - if (ca.Names[0] < cb.Names[0]) { + if (ca.containerName && cb.containerName) { + if (ca.containerName < cb.containerName) { return -1; } - if (ca.Names[0] > cb.Names[0]) { + if (ca.containerName > cb.containerName) { return 1; } } return 0; }) .map((containerInfo) => { - const outdated = - containerInfo.current.version !== - containerInfo.latest.version || - containerInfo.current.hash !== - containerInfo.latest.hash; + const outdated = containerInfo.current.hash !== containerInfo.latest.hash; return ( - - - {containerInfo.Names[0]?.substring(1)} - - {containerInfo.Image.split(":")[0]} - {containerInfo.current.version} + + {containerInfo.containerName} + {containerInfo.imageName} + {containerInfo.current.tag} {containerInfo.current.hash} - {containerInfo.latest.version} + {containerInfo.latest.tag} {containerInfo.latest.hash} ); diff --git a/src/server/api/routers/docker.ts b/src/server/api/routers/docker.ts index f301b51..b4c0c3b 100644 --- a/src/server/api/routers/docker.ts +++ b/src/server/api/routers/docker.ts @@ -18,38 +18,46 @@ export const dockerRouter = createTRPCRouter({ // curl -fsSL -v -H "Accept: application/json" "hub.docker.com/v2/repositories/linuxserver/code-server/tags?n=25&ordering=last_updated" - return Promise.all( + const dockerInfo = await Promise.all( containers.map(async (container) => { - const version = ( - await docker.getImage(container.Image).inspect() - ).RepoTags[0]?.split(":")[1]; - const latest = { - version: version, - hash: "N/A", + const imageInspect = await docker.getImage(container.Image).inspect(); + const imageDigest = imageInspect.RepoDigests[0]; + const containerInspect = docker.getContainer(container.Id); + + let imageTag = imageInspect.RepoTags[0]?.split(":")[1]; + + let imageName = imageDigest?.split("@")?.[0]; + imageName ??= container.Image.split(":")[0] ?? undefined; + + const imageHash = imageDigest?.split("@")?.[1]?.split(":")?.[1]; + imageTag ??= (await containerInspect.inspect()).Config.Image.split(":")[1] ?? undefined; + + const current = { + hash: imageHash?.substring(0, 12), + tag: imageTag, }; - if (version && isSemver(version)) { - latest.version = await getLatestSemverTag(container.Image); - latest.hash = await getLatestHash( - `${container.Image.split(":")[0]}:${latest.version}`, - ); - } else { - latest.hash = await getLatestHash(container.Image); + + let latest: { + hash?: string; + tag?: string; + } = { + hash: "", + tag: "", + }; + if (imageName) { + latest = await getLatest(imageName, current.tag); } return { - ...container, - current: { - version, - hash: ( - await docker.getImage(container.Image).inspect() - ).RepoDigests[0] - ?.split(":")[1] - ?.substring(0, 12), - }, + containerName: container.Names[0]?.replace("/", ""), + imageName, + current, latest, }; }), ); + + return dockerInfo; }), }); @@ -58,27 +66,58 @@ function isSemver(tag: string): boolean { return !!semver.valid(removeV); } -async function getLatestSemverTag(image: string): Promise { +async function getLatestSemverTag(image: string, tag?: string): Promise<{ hash: string; tag?: string }> { try { - const allTags = - await $`bin/regctl tag ls --exclude 'version.*' --exclude 'unstable.*' --exclude '.*rc.*' --exclude 'lib.*' --exclude 'release.*' --exclude 'arm.* + const latest = { + hash: "", + tag: "", + }; + + const allTags = await $`bin/regctl tag ls --exclude 'version.*' --exclude 'unstable.*' --exclude '.*rc.*' --exclude 'lib.*' --exclude 'release.*' --exclude 'arm.* ' --exclude 'amd.*' ${image}`.text(); - const semverTags = allTags - .split("\n") - .slice(-20) - .filter((tag) => isSemver(tag)); - return semver.rsort(semverTags)[0] ?? "Error"; + const semverTags = allTags.split("\n").filter((tag) => isSemver(tag)); + const newestTag = semver.rsort(semverTags)[0]; + + let imageSh = image; + if (newestTag) { + imageSh += `:${newestTag}`; + latest.tag = newestTag; + } + const latestImage = await $`bin/regctl image digest ${imageSh}`.text(); + latest.hash = latestImage.split(":")?.[1]?.substring(0, 12) ?? "Error"; + + return latest; } catch (ex) { - return "Error"; + console.error((ex as unknown as Error).message); + return { + hash: "Error", + tag: "Error", + }; } } -async function getLatestHash(image: string): Promise { +async function getLatest(image: string, tag?: string): Promise<{ hash: string; tag?: string }> { try { - const latestImage = await $`bin/regctl image digest ${image}`.text(); - const latestHash = latestImage.split(":")?.[1]; - return latestHash?.substring(0, 12) ?? "Error"; + let latest: { hash: string; tag?: string } = { + hash: "", + tag: tag, + }; + if (tag && isSemver(tag)) { + latest = await getLatestSemverTag(image, tag); + } else { + let imageSh = image; + if (tag) { + imageSh += `:${tag}`; + } + const latestImage = await $`bin/regctl image digest ${imageSh}`.text(); + latest.hash = latestImage.split(":")?.[1]?.substring(0, 12) ?? "Error"; + } + return latest; } catch (ex) { - return "Error"; + console.error((ex as unknown as Error).message); + return { + hash: "Error", + tag: "Error", + }; } } diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index 7e21b79..135a521 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -42,8 +42,7 @@ const t = initTRPC.context().create({ ...shape, data: { ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, + zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, }, }; }, diff --git a/src/styles/globals.css b/src/styles/globals.css index c7f9d8d..7689669 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -5,6 +5,5 @@ } @theme { - --font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; } diff --git a/src/trpc/query-client.ts b/src/trpc/query-client.ts index 0c7c3d7..5bdb5f7 100644 --- a/src/trpc/query-client.ts +++ b/src/trpc/query-client.ts @@ -1,7 +1,4 @@ -import { - QueryClient, - defaultShouldDehydrateQuery, -} from "@tanstack/react-query"; +import { QueryClient, defaultShouldDehydrateQuery } from "@tanstack/react-query"; import SuperJSON from "superjson"; export const createQueryClient = () => @@ -14,9 +11,7 @@ export const createQueryClient = () => }, dehydrate: { serializeData: SuperJSON.serialize, - shouldDehydrateQuery: (query) => - defaultShouldDehydrateQuery(query) || - query.state.status === "pending", + shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === "pending", }, hydrate: { deserializeData: SuperJSON.deserialize, diff --git a/src/trpc/react.tsx b/src/trpc/react.tsx index 0fcda69..1954941 100644 --- a/src/trpc/react.tsx +++ b/src/trpc/react.tsx @@ -45,9 +45,7 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) { api.createClient({ links: [ loggerLink({ - enabled: (op) => - process.env.NODE_ENV === "development" || - (op.direction === "down" && op.result instanceof Error), + enabled: (op) => process.env.NODE_ENV === "development" || (op.direction === "down" && op.result instanceof Error), }), httpBatchStreamLink({ transformer: SuperJSON, diff --git a/src/trpc/server.ts b/src/trpc/server.ts index 5f2fc58..cede2a8 100644 --- a/src/trpc/server.ts +++ b/src/trpc/server.ts @@ -24,7 +24,4 @@ const createContext = cache(async () => { const getQueryClient = cache(createQueryClient); const caller = createCaller(createContext); -export const { trpc: api, HydrateClient } = createHydrationHelpers( - caller, - getQueryClient, -); +export const { trpc: api, HydrateClient } = createHydrationHelpers(caller, getQueryClient); diff --git a/tsconfig.json b/tsconfig.json index 26a8b53..deedfcb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,13 +30,6 @@ "@/*": ["./src/*"] } }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - "**/*.cjs", - "**/*.js", - ".next/types/**/*.ts" - ], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.js", ".next/types/**/*.ts"], "exclude": ["node_modules"] }