mirror of
				https://github.com/r-freeman/portfolio.git
				synced 2025-10-31 21:11:11 +00:00 
			
		
		
		
	This commit is contained in:
		
							parent
							
								
									d91c0eae02
								
							
						
					
					
						commit
						6f53996192
					
				
							
								
								
									
										4
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | node_modules | ||||||
|  | .git | ||||||
|  | .next | ||||||
|  | .env | ||||||
| @ -1,14 +1,11 @@ | |||||||
| SPOTIFY_CLIENT_ID= | SPOTIFY_CLIENT_ID= | ||||||
| SPOTIFY_CLIENT_SECRET= | SPOTIFY_CLIENT_SECRET= | ||||||
| SPOTIFY_REFRESH_TOKEN= | SPOTIFY_REFRESH_TOKEN= | ||||||
| NEXT_PUBLIC_SITE_URL=https://example.com | NEXT_PUBLIC_SITE_URL= | ||||||
| GITHUB_ACCESS_TOKEN= | GITHUB_ACCESS_TOKEN= | ||||||
| GITHUB_USERNAME= | GITHUB_USERNAME= | ||||||
| GITHUB_CLIENT_ID= | GITHUB_CLIENT_ID= | ||||||
| GITHUB_SECRET= | GITHUB_SECRET= | ||||||
| STATSFM_USERNAME= |  | ||||||
| GRAFANA_URL= |  | ||||||
| GRAFANA_TOKEN= |  | ||||||
| NEXT_PUBLIC_SUPABASE_URL= | NEXT_PUBLIC_SUPABASE_URL= | ||||||
| NEXT_PUBLIC_SUPABASE_ANON_KEY= | NEXT_PUBLIC_SUPABASE_ANON_KEY= | ||||||
| SUPABASE_SERVICE_ROLE_KEY= | SUPABASE_SERVICE_ROLE_KEY= | ||||||
| @ -1,19 +0,0 @@ | |||||||
| name: Gitea Actions Demo |  | ||||||
| run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀 |  | ||||||
| on: [push] |  | ||||||
| 
 |  | ||||||
| jobs: |  | ||||||
|   Explore-Gitea-Actions: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event." |  | ||||||
|       - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!" |  | ||||||
|       - run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}." |  | ||||||
|       - name: Check out repository code |  | ||||||
|         uses: actions/checkout@v4 |  | ||||||
|       - run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner." |  | ||||||
|       - run: echo "🖥️ The workflow is now ready to test your code on the runner." |  | ||||||
|       - name: List files in the repository |  | ||||||
|         run: | |  | ||||||
|           ls ${{ gitea.workspace }} |  | ||||||
|       - run: echo "🍏 This job's status is ${{ job.status }}." |  | ||||||
							
								
								
									
										59
									
								
								.gitea/workflows/publish.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								.gitea/workflows/publish.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | |||||||
|  | name: Build And Publish | ||||||
|  | run-name: ${{ gitea.actor }} runs ci pipeline | ||||||
|  | on: [ push ] | ||||||
|  | 
 | ||||||
|  | jobs: | ||||||
|  |   BuildAndPublish: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout code | ||||||
|  |         uses: https://github.com/actions/checkout@v4 | ||||||
|  | 
 | ||||||
|  |       - name: Use Node.js | ||||||
|  |         uses: https://github.com/actions/setup-node@v3 | ||||||
|  |         with: | ||||||
|  |           node-version: '18.17.0' | ||||||
|  | 
 | ||||||
|  |       - name: Decrypt secrets | ||||||
|  |         run: ./decrypt_secrets.sh | ||||||
|  |         env: | ||||||
|  |           SECRET_PASSPHRASE: ${{ secrets.SECRET_PASSPHRASE }} | ||||||
|  | 
 | ||||||
|  |       - name: Login to Docker Hub | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           username: ${{secrets.DOCKER_HUB_USERNAME}} | ||||||
|  |           password: ${{secrets.DOCKER_HUB_PASSWORD}} | ||||||
|  | 
 | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: https://github.com/docker/setup-buildx-action@v3 | ||||||
|  |         with: | ||||||
|  |           config-inline: | | ||||||
|  |             [registry."docker.io"] | ||||||
|  |               mirrors = ["mirror.gcr.io"]             | ||||||
|  | 
 | ||||||
|  |       - name: Build and push Docker image | ||||||
|  |         uses: https://github.com/docker/build-push-action@v6 | ||||||
|  |         with: | ||||||
|  |           context: . | ||||||
|  |           file: ./Dockerfile | ||||||
|  |           push: true | ||||||
|  |           tags: ${{secrets.DOCKER_HUB_USERNAME}}/portfolio:v1 | ||||||
|  |           secrets: | | ||||||
|  |             "NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" | ||||||
|  |             "NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" | ||||||
|  |             "SUPABASE_SERVICE_ROLE_KEY=${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" | ||||||
|  | 
 | ||||||
|  |       - name: Stop the docker container | ||||||
|  |         continue-on-error: true | ||||||
|  |         run: sudo docker stop portfolio | ||||||
|  | 
 | ||||||
|  |       - name: Remove the docker container | ||||||
|  |         continue-on-error: true | ||||||
|  |         run: sudo docker rm portfolio | ||||||
|  | 
 | ||||||
|  |       - name: Pull the Docker image | ||||||
|  |         run: sudo docker pull ${{secrets.DOCKER_HUB_USERNAME}}/portfolio:v1 | ||||||
|  | 
 | ||||||
|  |       - name: Run the Docker container | ||||||
|  |         run: sudo docker run -d --restart unless-stopped --env-file ./.env --name portfolio -p ${{vars.TAILSCALE_IP}}:3000:3000 ${{secrets.DOCKER_HUB_USERNAME}}/portfolio:v1 | ||||||
							
								
								
									
										79
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,79 @@ | |||||||
|  | FROM node:18-alpine AS base | ||||||
|  | 
 | ||||||
|  | # Install dependencies only when needed | ||||||
|  | FROM base AS deps | ||||||
|  | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. | ||||||
|  | RUN apk add --no-cache libc6-compat | ||||||
|  | WORKDIR /app | ||||||
|  | 
 | ||||||
|  | # Install dependencies based on the preferred package manager | ||||||
|  | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ | ||||||
|  | RUN \ | ||||||
|  |   if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ | ||||||
|  |   elif [ -f package-lock.json ]; then npm ci --legacy-peer-deps; \ | ||||||
|  |   elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ | ||||||
|  |   else echo "Lockfile not found." && exit 1; \ | ||||||
|  |   fi | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Rebuild the source code only when needed | ||||||
|  | FROM base AS builder | ||||||
|  | WORKDIR /app | ||||||
|  | COPY --from=deps /app/node_modules ./node_modules | ||||||
|  | COPY . . | ||||||
|  | 
 | ||||||
|  | # Next.js collects completely anonymous telemetry data about general usage. | ||||||
|  | # Learn more here: https://nextjs.org/telemetry | ||||||
|  | # Uncomment the following line in case you want to disable telemetry during the build. | ||||||
|  | ENV NEXT_TELEMETRY_DISABLED=1 | ||||||
|  | 
 | ||||||
|  | RUN --mount=type=secret,id=NEXT_PUBLIC_SUPABASE_URL,required ls -la /run/secrets/ | ||||||
|  | 
 | ||||||
|  | #RUN --mount=type=secret,id=NEXT_PUBLIC_SUPABASE_URL export NEXT_PUBLIC_SUPABASE_URL=$(cat /run/secrets/NEXT_PUBLIC_SUPABASE_URL) | ||||||
|  | #RUN --mount=type=secret,id=NEXT_PUBLIC_SUPABASE_URL export NEXT_PUBLIC_SUPABASE_URL=$(cat /run/secrets/NEXT_PUBLIC_SUPABASE_URL) | ||||||
|  | #  --mount=type=secret,id=NEXT_PUBLIC_SUPABASE_ANON_KEY \ | ||||||
|  | #  --mount=type=secret,id=SUPABASE_SERVICE_ROLE_KEY \ | ||||||
|  | #   export NEXT_PUBLIC_SUPABASE_ANON_KEY=$(cat /run/secrets/NEXT_PUBLIC_SUPABASE_ANON_KEY) && \ | ||||||
|  | #   export SUPABASE_SERVICE_ROLE_KEY=$(cat /run/secrets/SUPABASE_SERVICE_ROLE_KEY) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | RUN \ | ||||||
|  |   if [ -f yarn.lock ]; then yarn run build; \ | ||||||
|  |   elif [ -f package-lock.json ]; then npm run build; \ | ||||||
|  |   elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ | ||||||
|  |   else echo "Lockfile not found." && exit 1; \ | ||||||
|  |   fi | ||||||
|  | 
 | ||||||
|  | # Production image, copy all the files and run next | ||||||
|  | FROM base AS runner | ||||||
|  | WORKDIR /app | ||||||
|  | 
 | ||||||
|  | ENV NODE_ENV=production | ||||||
|  | # Uncomment the following line in case you want to disable telemetry during runtime. | ||||||
|  | ENV NEXT_TELEMETRY_DISABLED=1 | ||||||
|  | 
 | ||||||
|  | RUN addgroup --system --gid 1001 nodejs | ||||||
|  | RUN adduser --system --uid 1001 nextjs | ||||||
|  | 
 | ||||||
|  | COPY --from=builder /app/public ./public | ||||||
|  | 
 | ||||||
|  | # Set the correct permission for prerender cache | ||||||
|  | RUN mkdir .next | ||||||
|  | RUN chown nextjs:nodejs .next | ||||||
|  | 
 | ||||||
|  | # Automatically leverage output traces to reduce image size | ||||||
|  | # https://nextjs.org/docs/advanced-features/output-file-tracing | ||||||
|  | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ | ||||||
|  | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static | ||||||
|  | 
 | ||||||
|  | USER nextjs | ||||||
|  | 
 | ||||||
|  | EXPOSE 3000 | ||||||
|  | 
 | ||||||
|  | ENV PORT=3000 | ||||||
|  | 
 | ||||||
|  | ENV HOSTNAME="0.0.0.0" | ||||||
|  | 
 | ||||||
|  | # server.js is created by next build from the standalone output | ||||||
|  | # https://nextjs.org/docs/pages/api-reference/next-config-js/output | ||||||
|  | CMD ["node", "server.js"] | ||||||
| @ -1,26 +0,0 @@ | |||||||
| import {NextResponse} from 'next/server' |  | ||||||
| import {getRamUsage, getRootFsUsage, getSysLoad, getTemp, getUptime} from '@/lib/pi' |  | ||||||
| 
 |  | ||||||
| export const fetchCache = 'force-no-store' |  | ||||||
| 
 |  | ||||||
| export async function GET(request: Request, {params}: { params: { slug: string } }) { |  | ||||||
|     const slug = params.slug |  | ||||||
|     let response |  | ||||||
|     if (slug === 'ram') { |  | ||||||
|         response = await getRamUsage() |  | ||||||
|     } else if (slug === 'rootfs') { |  | ||||||
|         response = await getRootFsUsage() |  | ||||||
|     } else if (slug === 'sysload') { |  | ||||||
|         response = await getSysLoad() |  | ||||||
|     } else if (slug === 'temp') { |  | ||||||
|         response = await getTemp() |  | ||||||
|     } else if (slug === 'uptime') { |  | ||||||
|         response = await getUptime() |  | ||||||
|     } else { |  | ||||||
|         return new Response('Not Found', { |  | ||||||
|             status: 404 |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return NextResponse.json(response) |  | ||||||
| } |  | ||||||
| @ -9,6 +9,7 @@ export async function GET(request: Request, {params}: { params: { slug: string } | |||||||
|             const supabase = createServerComponentClient<Database>({cookies}) |             const supabase = createServerComponentClient<Database>({cookies}) | ||||||
|             const slug = params.slug.toString() |             const slug = params.slug.toString() | ||||||
|             const response = await supabase |             const response = await supabase | ||||||
|  |                 // @ts-ignore
 | ||||||
|                 .from('analytics') |                 .from('analytics') | ||||||
|                 .select('views') |                 .select('views') | ||||||
|                 .eq('slug', slug) |                 .eq('slug', slug) | ||||||
|  | |||||||
							
								
								
									
										5
									
								
								decrypt_secrets.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								decrypt_secrets.sh
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | #!/bin/sh | ||||||
|  | 
 | ||||||
|  | # --batch to prevent interactive command | ||||||
|  | # --yes to assume "yes" for questions | ||||||
|  | gpg --quiet --batch --yes --decrypt --passphrase="$SECRET_PASSPHRASE" --output ./.env ./.env.gpg | ||||||
							
								
								
									
										145
									
								
								lib/dashboard.ts
									
									
									
									
									
								
							
							
						
						
									
										145
									
								
								lib/dashboard.ts
									
									
									
									
									
								
							| @ -1,145 +0,0 @@ | |||||||
| import {cookies} from 'next/headers' |  | ||||||
| import {createServerComponentClient} from '@supabase/auth-helpers-nextjs' |  | ||||||
| import {getTopRepo, getTotalFollowers, getTotalForks, getTotalRepos, getTotalStars} from '@/lib/github' |  | ||||||
| import {getAllArticles} from '@/lib/getAllArticles' |  | ||||||
| import {getTopArtist, getTopGenre} from '@/lib/spotify' |  | ||||||
| import {getRamUsage, getRootFsUsage, getSysLoad, getTemp, getUptime} from '@/lib/pi' |  | ||||||
| import {getStats} from '@/lib/statsfm' |  | ||||||
| import {Metric} from '@/types' |  | ||||||
| 
 |  | ||||||
| export async function getDashboardData() { |  | ||||||
|     const supabase = createServerComponentClient({cookies}) |  | ||||||
|     const {data: views} = await supabase.rpc('total_views') |  | ||||||
|     const [totalRepos, totalFollowers] = await Promise.all([ |  | ||||||
|         getTotalRepos(), |  | ||||||
|         getTotalFollowers() |  | ||||||
|     ]) |  | ||||||
| 
 |  | ||||||
|     const topRepo = await getTopRepo() |  | ||||||
|     const totalStars = await getTotalStars(totalRepos) |  | ||||||
|     const totalForks = await getTotalForks(totalRepos) |  | ||||||
|     const totalArticles = (await getAllArticles()).length |  | ||||||
|     const topArtist = await getTopArtist() |  | ||||||
|     const {genre} = await getTopGenre() |  | ||||||
|     const {hoursListened, minutesListened, streams} = await getStats() |  | ||||||
|     const {temp} = await getTemp() |  | ||||||
|     const {sysLoad} = await getSysLoad() |  | ||||||
|     const {ramUsage} = await getRamUsage() |  | ||||||
|     const {rootFsUsage} = await getRootFsUsage() |  | ||||||
|     const {days} = await getUptime() |  | ||||||
| 
 |  | ||||||
|     const metrics: Metric[] = [ |  | ||||||
|         { |  | ||||||
|             title: "Streams", |  | ||||||
|             value: +streams, |  | ||||||
|             group: "Spotify", |  | ||||||
|             href: "https://open.spotify.com/?" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Hours listened", |  | ||||||
|             value: +hoursListened, |  | ||||||
|             group: "Spotify", |  | ||||||
|             href: "https://open.spotify.com/?" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Minutes listened", |  | ||||||
|             value: +minutesListened, |  | ||||||
|             group: "Spotify", |  | ||||||
|             href: "https://open.spotify.com/?" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Top genre", |  | ||||||
|             value: genre, |  | ||||||
|             group: "Spotify", |  | ||||||
|             href: "https://open.spotify.com/?" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Top artist", |  | ||||||
|             value: topArtist.artist, |  | ||||||
|             group: "Spotify", |  | ||||||
|             href: topArtist.uri |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Repos", |  | ||||||
|             value: +totalRepos, |  | ||||||
|             group: "GitHub", |  | ||||||
|             href: "https://github.com/r-freeman?tab=repositories" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Top repo", |  | ||||||
|             value: topRepo.name, |  | ||||||
|             group: "GitHub", |  | ||||||
|             href: topRepo.url |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Followers", |  | ||||||
|             value: +totalFollowers, |  | ||||||
|             group: "GitHub", |  | ||||||
|             href: "https://github.com/r-freeman?tab=followers" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Stars", |  | ||||||
|             value: +totalStars, |  | ||||||
|             group: "GitHub", |  | ||||||
|             href: "https://github.com/r-freeman/" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Forks", |  | ||||||
|             value: +totalForks, |  | ||||||
|             group: "GitHub", |  | ||||||
|             href: "https://github.com/r-freeman/" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Total articles", |  | ||||||
|             value: +totalArticles, |  | ||||||
|             group: "Blog", |  | ||||||
|             href: "/writing" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Total article views", |  | ||||||
|             value: +views, |  | ||||||
|             group: "Blog", |  | ||||||
|             href: "/writing" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Temp", |  | ||||||
|             value: `${temp} ℃`, |  | ||||||
|             group: "Raspberry Pi", |  | ||||||
|             href: "" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Sys load (5m avg)", |  | ||||||
|             value: `${sysLoad}%`, |  | ||||||
|             group: "Raspberry Pi", |  | ||||||
|             href: "" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "RAM usage", |  | ||||||
|             value: `${ramUsage}%`, |  | ||||||
|             group: "Raspberry Pi", |  | ||||||
|             href: "" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Root FS usage", |  | ||||||
|             value: `${rootFsUsage}%`, |  | ||||||
|             group: "Raspberry Pi", |  | ||||||
|             href: "" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             title: "Uptime days", |  | ||||||
|             value: `${Math.round(days)}`, |  | ||||||
|             group: "Raspberry Pi", |  | ||||||
|             href: "" |  | ||||||
|         } |  | ||||||
|     ] |  | ||||||
| 
 |  | ||||||
|     // sort metrics into named groups
 |  | ||||||
|     const groups = metrics.reduce((acc: { [key: string]: Metric[] }, item) => { |  | ||||||
|         (acc[item.group] = acc[item.group] || []).push(item); |  | ||||||
|         return acc |  | ||||||
|     }, {} as { [key: string]: Metric[] }) |  | ||||||
| 
 |  | ||||||
|     return Object.entries(groups).map(([groupName, groupItems]) => { |  | ||||||
|         return {groupName, groupItems} |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
							
								
								
									
										170
									
								
								lib/pi.ts
									
									
									
									
									
								
							
							
						
						
									
										170
									
								
								lib/pi.ts
									
									
									
									
									
								
							| @ -1,170 +0,0 @@ | |||||||
| import fetcher from '@/lib/fetcher' |  | ||||||
| 
 |  | ||||||
| const GRAFANA_URL: string = process.env.GRAFANA_URL ?? "" |  | ||||||
| const GRAFANA_TOKEN = process.env.GRAFANA_TOKEN |  | ||||||
| 
 |  | ||||||
| const day = 24 * 60 * 60 * 1000; |  | ||||||
| const yesterday = Date.now() - day; |  | ||||||
| 
 |  | ||||||
| export const getTemp = async () => { |  | ||||||
|     const response = await fetcher(GRAFANA_URL, { |  | ||||||
|         method: 'POST', |  | ||||||
|         headers: { |  | ||||||
|             Authorization: `Bearer ${GRAFANA_TOKEN}`, |  | ||||||
|             'Accept': 'application/json', |  | ||||||
|             'Content-Type': 'application/json' |  | ||||||
|         }, |  | ||||||
|         body: JSON.stringify({ |  | ||||||
|             "queries": [ |  | ||||||
|                 { |  | ||||||
|                     "datasource": { |  | ||||||
|                         "uid": "4f-R6jgRz", |  | ||||||
|                         "type": "prometheus" |  | ||||||
|                     }, |  | ||||||
|                     "expr": "node_hwmon_temp_celsius", |  | ||||||
|                     "maxDataPoints": 100 |  | ||||||
|                 } |  | ||||||
|             ], |  | ||||||
|             "from": yesterday.toString(), |  | ||||||
|             "to": "now" |  | ||||||
|         }) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     const temp = parseInt(response.results.A.frames[0].data.values[1].slice(-1)) |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|         temp |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const getRootFsUsage = async () => { |  | ||||||
|     const response = await fetcher(GRAFANA_URL, { |  | ||||||
|         method: 'POST', |  | ||||||
|         headers: { |  | ||||||
|             Authorization: `Bearer ${GRAFANA_TOKEN}`, |  | ||||||
|             'Accept': 'application/json', |  | ||||||
|             'Content-Type': 'application/json' |  | ||||||
|         }, |  | ||||||
|         body: JSON.stringify({ |  | ||||||
|             "queries": [ |  | ||||||
|                 { |  | ||||||
|                     "datasource": { |  | ||||||
|                         "uid": "4f-R6jgRz", |  | ||||||
|                         "type": "prometheus" |  | ||||||
|                     }, |  | ||||||
|                     "expr": "100 - ((node_filesystem_avail_bytes{mountpoint='/',fstype!='rootfs'} * 100) / node_filesystem_size_bytes{mountpoint='/',fstype!='rootfs'})", |  | ||||||
|                     "maxDataPoints": 100 |  | ||||||
|                 } |  | ||||||
|             ], |  | ||||||
|             "from": yesterday.toString(), |  | ||||||
|             "to": "now" |  | ||||||
|         }) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     const rootFsUsage = parseInt(response.results.A.frames[0].data.values[1].slice(-1)) |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|         rootFsUsage |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const getUptime = async () => { |  | ||||||
|     const response = await fetcher(GRAFANA_URL, { |  | ||||||
|         method: 'POST', |  | ||||||
|         headers: { |  | ||||||
|             Authorization: `Bearer ${GRAFANA_TOKEN}`, |  | ||||||
|             'Accept': 'application/json', |  | ||||||
|             'Content-Type': 'application/json' |  | ||||||
|         }, |  | ||||||
|         body: JSON.stringify({ |  | ||||||
|             "queries": [ |  | ||||||
|                 { |  | ||||||
|                     "datasource": { |  | ||||||
|                         "uid": "4f-R6jgRz", |  | ||||||
|                         "type": "prometheus" |  | ||||||
|                     }, |  | ||||||
|                     "expr": "node_time_seconds - node_boot_time_seconds", |  | ||||||
|                     "maxDataPoints": 100 |  | ||||||
|                 } |  | ||||||
|             ], |  | ||||||
|             "from": yesterday.toString(), |  | ||||||
|             "to": "now" |  | ||||||
|         }) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     const seconds = parseInt(response.results.A.frames[0].data.values[1].slice(-1)) |  | ||||||
|     const minutes = (seconds / 60) |  | ||||||
|     const hours = (seconds / 3_600) |  | ||||||
|     const days = (seconds / 86400) |  | ||||||
|     const weeks = (seconds / 604_800) |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|         seconds: (seconds), |  | ||||||
|         minutes, |  | ||||||
|         hours, |  | ||||||
|         days, |  | ||||||
|         weeks |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const getRamUsage = async () => { |  | ||||||
|     const response = await fetcher(GRAFANA_URL, { |  | ||||||
|         method: 'POST', |  | ||||||
|         headers: { |  | ||||||
|             Authorization: `Bearer ${GRAFANA_TOKEN}`, |  | ||||||
|             'Accept': 'application/json', |  | ||||||
|             'Content-Type': 'application/json' |  | ||||||
|         }, |  | ||||||
|         body: JSON.stringify({ |  | ||||||
|             "queries": [ |  | ||||||
|                 { |  | ||||||
|                     "datasource": { |  | ||||||
|                         "uid": "4f-R6jgRz", |  | ||||||
|                         "type": "prometheus" |  | ||||||
|                     }, |  | ||||||
|                     "expr": "100 - ((node_memory_MemAvailable_bytes * 100) / node_memory_MemTotal_bytes)", |  | ||||||
|                     "maxDataPoints": 100 |  | ||||||
|                 } |  | ||||||
|             ], |  | ||||||
|             "from": yesterday.toString(), |  | ||||||
|             "to": "now" |  | ||||||
|         }) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     const ramUsage = parseInt(response.results.A.frames[0].data.values[1].slice(-1)) |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|         ramUsage |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const getSysLoad = async () => { |  | ||||||
|     const response = await fetcher(GRAFANA_URL, { |  | ||||||
|         method: 'POST', |  | ||||||
|         headers: { |  | ||||||
|             Authorization: `Bearer ${GRAFANA_TOKEN}`, |  | ||||||
|             'Accept': 'application/json', |  | ||||||
|             'Content-Type': 'application/json' |  | ||||||
|         }, |  | ||||||
|         body: JSON.stringify({ |  | ||||||
|             "queries": [ |  | ||||||
|                 { |  | ||||||
|                     "datasource": { |  | ||||||
|                         "uid": "4f-R6jgRz", |  | ||||||
|                         "type": "prometheus" |  | ||||||
|                     }, |  | ||||||
|                     "expr": "avg(node_load5) /  count(count(node_cpu_seconds_total) by (cpu)) * 100", |  | ||||||
|                     "maxDataPoints": 100 |  | ||||||
|                 } |  | ||||||
|             ], |  | ||||||
|             "from": yesterday.toString(), |  | ||||||
|             "to": "now" |  | ||||||
|         }) |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     const sysLoad = parseInt(response.results.A.frames[0].data.values[1].slice(-1)) |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|         sysLoad |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,31 +0,0 @@ | |||||||
| import fetcher from './fetcher' |  | ||||||
| 
 |  | ||||||
| const STATSFM_USERNAME = process.env.STATSFM_USERNAME |  | ||||||
| const STATSFM_LIFETIME_STATS = `https://beta-api.stats.fm/api/v1/users/${STATSFM_USERNAME}/streams/stats?range=lifetime` |  | ||||||
| 
 |  | ||||||
| type StatsFmResponse = { |  | ||||||
|     items: { |  | ||||||
|         durationMs: number |  | ||||||
|         count: number |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const getStats = async () => { |  | ||||||
|     const response = await fetcher(STATSFM_LIFETIME_STATS, { |  | ||||||
|         method: 'GET', |  | ||||||
|         headers: { |  | ||||||
|             'Accept': 'application/json' |  | ||||||
|         } |  | ||||||
|     }) as StatsFmResponse |  | ||||||
| 
 |  | ||||||
|     const {durationMs} = response.items |  | ||||||
|     const hoursListened = (durationMs / 3_600_000).toFixed(0) |  | ||||||
|     const minutesListened = (durationMs / 60_000).toFixed(0) |  | ||||||
|     const streams = response.items.count |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|         hoursListened, |  | ||||||
|         minutesListened, |  | ||||||
|         streams |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -11,7 +11,8 @@ const nextConfig = { | |||||||
|             hostname: 'i.scdn.co', |             hostname: 'i.scdn.co', | ||||||
|             port: '' |             port: '' | ||||||
|         }] |         }] | ||||||
|     } |     }, | ||||||
|  |     output: 'standalone' | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const withMDX = nextMDX({ | const withMDX = nextMDX({ | ||||||
|  | |||||||
							
								
								
									
										9355
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9355
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -33,7 +33,7 @@ | |||||||
|     "feed": "^4.2.2", |     "feed": "^4.2.2", | ||||||
|     "focus-visible": "^5.2.0", |     "focus-visible": "^5.2.0", | ||||||
|     "motion": "^10.15.5", |     "motion": "^10.15.5", | ||||||
|     "next": "^14.1.0", |     "next": "^14.1.1", | ||||||
|     "next-themes": "^0.2.1", |     "next-themes": "^0.2.1", | ||||||
|     "postcss": "^8.4.21", |     "postcss": "^8.4.21", | ||||||
|     "postcss-focus-visible": "^7.1.0", |     "postcss-focus-visible": "^7.1.0", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user