mirror of
https://github.com/r-freeman/portfolio.git
synced 2024-11-24 14:55:41 +00:00
This commit is contained in:
parent
d91c0eae02
commit
0327749390
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 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)
|
||||||
|
#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…
Reference in New Issue
Block a user