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_SECRET= | ||||
| SPOTIFY_REFRESH_TOKEN= | ||||
| NEXT_PUBLIC_SITE_URL=https://example.com | ||||
| NEXT_PUBLIC_SITE_URL= | ||||
| GITHUB_ACCESS_TOKEN= | ||||
| GITHUB_USERNAME= | ||||
| GITHUB_CLIENT_ID= | ||||
| GITHUB_SECRET= | ||||
| STATSFM_USERNAME= | ||||
| GRAFANA_URL= | ||||
| GRAFANA_TOKEN= | ||||
| NEXT_PUBLIC_SUPABASE_URL= | ||||
| NEXT_PUBLIC_SUPABASE_ANON_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 slug = params.slug.toString() | ||||
|             const response = await supabase | ||||
|                 // @ts-ignore
 | ||||
|                 .from('analytics') | ||||
|                 .select('views') | ||||
|                 .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', | ||||
|             port: '' | ||||
|         }] | ||||
|     } | ||||
|     }, | ||||
|     output: 'standalone' | ||||
| } | ||||
| 
 | ||||
| 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", | ||||
|     "focus-visible": "^5.2.0", | ||||
|     "motion": "^10.15.5", | ||||
|     "next": "^14.1.0", | ||||
|     "next": "^14.1.1", | ||||
|     "next-themes": "^0.2.1", | ||||
|     "postcss": "^8.4.21", | ||||
|     "postcss-focus-visible": "^7.1.0", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user