Compare commits

..

7 Commits

Author SHA1 Message Date
a463e61fb4 Update readme
All checks were successful
Build and deploy / deploy (push) Successful in 1m15s
2025-09-06 16:44:30 +01:00
joe
7b0ff36052 Merge pull request 'AddMobile' (#1) from AddMobile into main
All checks were successful
Build and deploy / deploy (push) Successful in 1m19s
Reviewed-on: #1
2025-09-06 16:22:59 +01:00
00c32e7cdf Switch to cards 2025-09-06 16:22:22 +01:00
21caaf3901 Look at adding mobile cards 2025-09-06 01:07:13 +01:00
590fe84b40 That's why I had certs... Update regctl
All checks were successful
Build and deploy / deploy (push) Successful in 1m24s
2025-09-05 00:51:28 +01:00
32f53eb27e alpine sucks sometimes
All checks were successful
Build and deploy / deploy (push) Successful in 1m23s
2025-09-05 00:39:53 +01:00
eb23299181 Update dockerfile for node
All checks were successful
Build and deploy / deploy (push) Successful in 1m17s
2025-09-04 23:21:03 +01:00
7 changed files with 112 additions and 94 deletions

View File

@@ -1,5 +1,5 @@
# Use the official Node.js image as the base image
FROM node:24-alpine
FROM node:24-trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
@@ -17,6 +17,8 @@ RUN npm ci
# Copy the rest of the application files
COPY . .
RUN chmod +x ./bin/regctl
# Build the NestJS application
RUN npm run build

View File

@@ -1,29 +1,30 @@
# Create T3 App
# DVDash
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
## What's next? How do I make an app with this?
We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary.
If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
- [Next.js](https://nextjs.org)
- [NextAuth.js](https://next-auth.js.org)
- [Prisma](https://prisma.io)
- [Drizzle](https://orm.drizzle.team)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)
## Learn More
To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
- [Documentation](https://create.t3.gg/)
- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
A little dashboard for checking the latest versions of running docker containers.
Uses [dockerode](https://www.npmjs.com/package/dockerode) to check running containers, grab their images and then uses [regctl](https://github.com/regclient/regclient) to handle the connection to any registry to get the latest semver version for the image if found.
I used regctl because it turns out loads of docker registries do not follow the [OCI spec](https://github.com/opencontainers/distribution-spec/blob/main/spec.md) and that tool just deals with it for me.
## How do I deploy this?
Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.
This just runs on port 3000 and needs the docker socket as read only.
```yaml
dvdash:
container_name: dvdash
image: gitea.home.joemonk.co.uk/joe/dvdash:1
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
3000:3000
```
## Create T3 App
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app` (because I'm comfortable with it and wanted to build a very small application very quickly).
### Learn More
- [Next.js](https://nextjs.org)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)

Binary file not shown.

View File

@@ -4,90 +4,103 @@ import type { dockerRouterType } from "@/server/api/routers/docker";
import { api } from "@/trpc/react";
import type { JSX } from "react";
function DockerRow({
function DockerCard({
containerInfo,
}: {
containerInfo: dockerRouterType["list"][number];
}) {
const { data: latest, isError, isLoading } = api.docker.latest.useQuery({ id: containerInfo.container.id });
const outdated = containerInfo.image.current.hash !== latest?.latest.hash;
const { data: latest, isError, isLoading, error } = api.docker.latest.useQuery({ id: containerInfo.container.id }, { refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false });
let latestFragment: JSX.Element | null = null;
if (isError) {
latestFragment = (
<>
<td>{"Error"}</td>
<td>{"Error"}</td>
</>
<div className="flex flex-row flex-wrap *:basis-1/2">
<dt>Tag</dt>
<dd>Error: {error.message}</dd>
<dt>Short Hash</dt>
<dd>Error: {error.message}</dd>
</div>
);
} else if (isLoading) {
latestFragment = (
<>
<td>
<span className="loading loading-dots loading-lg" />
</td>
<td>
<span className="loading loading-dots loading-lg" />
</td>
</>
<div className="flex flex-row flex-wrap *:basis-1/2">
<dt>Tag</dt>
<dd className="loading loading-dots loading-sm" />
<dt>Short Hash</dt>
<dd className="loading loading-dots loading-sm" />
</div>
);
} else if (latest) {
latestFragment = (
<>
<td>{latest?.latest.tag}</td>
<td>{latest?.latest.hash}</td>
</>
<div className="flex flex-row flex-wrap *:basis-1/2">
<dt>Tag</dt>
<dd>{latest?.latest.tag ?? "Unknown"}</dd>
<dt>Short Hash</dt>
<dd>{latest?.latest.hash ?? "Unknown"}</dd>
</div>
);
}
const outdated = containerInfo.image.current.hash !== latest?.latest.hash;
const showWarning = isLoading || (!isError && !isLoading && (!latest?.latest.tag || !latest?.latest.hash));
return (
<tr key={containerInfo.container.name} className={`${outdated ? "bg-base-200" : null}`}>
<td className={`border-l-8 ${isLoading ? "border-l-warning/80" : outdated ? "border-l-error/80" : "border-l-info/80"}`}>{containerInfo.container.name}</td>
<td>{containerInfo.image.name}</td>
<td>{containerInfo.image.current.tag}</td>
<td>{containerInfo.image.current.hash}</td>
{latestFragment}
</tr>
<div
key={containerInfo.container.name}
className={`card card-outline card-compact w-80 border-t-8 bg-base-300 shadow-md ${showWarning ? "border-t-warning/80" : outdated ? "border-t-error/80" : "border-t-info/80"}`}
>
<dl className="card-body flex flex-col flex-wrap items-center justify-around text-base">
<h2 className="text-center text-3xl">{containerInfo.container.name}</h2>
<p>{containerInfo.image.name}</p>
<div className="divider divider-primary m-1" />
<h3 className="text-xl underline decoration-1 underline-offset-4">Current</h3>
<div className="flex flex-row flex-wrap *:basis-1/2">
<dt>Tag</dt>
<dd>{containerInfo.image.current.tag}</dd>
<dt>Short Hash</dt>
<dd>{containerInfo.image.current.hash}</dd>
</div>
<div className="divider divider-primary m-0" />
<h3 className="text-xl underline decoration-1 underline-offset-4">Latest</h3>
{latestFragment}
</dl>
</div>
);
}
export function DockerTable() {
const { data: list, isLoading: listLoading } = api.docker.list.useQuery();
const { data: list, isLoading: listLoading, error } = api.docker.list.useQuery(undefined, { refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false });
return (
<div className="overflow-x-auto rounded-md border border-base-content/15 bg-base-100">
{!listLoading ? (
<table className="table-s table">
<thead>
<tr>
<th>Name</th>
<th>Image</th>
<th>Tag</th>
<th>Short Hash</th>
<th>Tag</th>
<th>Short Hash</th>
</tr>
</thead>
<tbody>
{list
? list
.sort((ca, cb) => {
if (ca.container.name && cb.container.name) {
if (ca.container.name < cb.container.name) {
return -1;
}
if (ca.container.name > cb.container.name) {
return 1;
}
}
return 0;
})
.map((containerInfo) => <DockerRow key={containerInfo.container.name} containerInfo={containerInfo} />)
: null}
</tbody>
</table>
<>
{listLoading ? (
<>Loading docker container list...</>
) : (
<>Loading data...</>
<>
<div className={"flex flex-row flex-wrap justify-around gap-8"}>
{list ? (
list
.sort((ca, cb) => {
if (ca.container.name && cb.container.name) {
if (ca.container.name < cb.container.name) {
return -1;
}
if (ca.container.name > cb.container.name) {
return 1;
}
}
return 0;
})
.map((containerInfo) => <DockerCard key={containerInfo.container.name} containerInfo={containerInfo} />)
) : (
<>Error loading docker container list: {error?.message}</>
)}
</div>
</>
)}
</div>
</>
);
}

View File

@@ -5,10 +5,8 @@ export default async function Home() {
void api.docker.list.prefetch();
return (
<main className="flex min-h-screen flex-col items-center justify-center">
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
<DockerTable />
</div>
<main className="container flex min-h-screen flex-col items-center justify-center px-4 py-8">
<DockerTable />
</main>
);
}

View File

@@ -14,6 +14,7 @@ export const dockerRouter = createTRPCRouter({
const container = containers.find((container) => container.Id === input.id);
if (!container) {
console.error(`Container with id ${input.id} not found`);
throw new TRPCError({
code: "NOT_FOUND",
message: `Container with id ${input.id} not found`,
@@ -27,6 +28,7 @@ export const dockerRouter = createTRPCRouter({
latest: await getLatest(imageData.name, imageData.tag),
};
} catch (ex) {
console.error(ex);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: (ex as Error).message,
@@ -159,9 +161,10 @@ function isSemver(tag: string): boolean {
}
async function getHash(image: string) {
const latestImage = await $`bin/regctl image digest ${image}`.text();
const latestImage = await $`./bin/regctl image digest ${image}`.text();
const hash = latestImage.split(":")?.[1]?.substring(0, 12);
if (!hash) {
console.error(`Hash not found: ${latestImage}`);
throw new Error("Hash not found", {
cause: {
imageDigest: latestImage,
@@ -185,12 +188,13 @@ async function getLatest(image: string, tag?: string): Promise<{ hash: string; t
return {
hash,
tag,
};
}
async function getSemverTag(image: string): Promise<{ hash: string; tag?: string }> {
const allTags = await $`bin/regctl tag ls --exclude 'version.*' --exclude 'unstable.*' --exclude '.*rc.*' --exclude 'lib.*' --exclude 'release.*' --exclude 'arm.*
' --exclude 'amd.*' ${image}`.text();
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").filter((tag) => isSemver(tag));
const newestTag = semver.rsort(semverTags)[0];

View File

@@ -6,4 +6,4 @@
@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";
}
}