Skip to content

Commit

Permalink
progress so far
Browse files Browse the repository at this point in the history
  • Loading branch information
Fallen-Breath committed Jan 16, 2025
1 parent 3081ab8 commit 936c982
Show file tree
Hide file tree
Showing 16 changed files with 789 additions and 7 deletions.
87 changes: 87 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,24 @@
"@mantine/nprogress": "^7.15.1",
"@next/bundle-analyzer": "15.1.2",
"@tabler/icons-react": "^3.26.0",
"@types/chroma-js": "^3.1.0",
"@vercel/analytics": "^1.4.1",
"@vercel/speed-insights": "^1.1.0",
"chart.js": "^4.4.7",
"chartjs-adapter-dayjs-4": "^1.0.4",
"chartjs-plugin-datalabels": "^2.2.0",
"chroma-js": "^3.1.2",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"github-markdown-css": "^5.8.1",
"hast-util-from-html": "^2.0.3",
"highlight.js": "^11.11.0",
"i18n-iso-countries": "^7.13.0",
"mermaid": "^11.4.1",
"next": "15.1.2",
"next-intl": "3.26.2",
"react": "19.0.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "19.0.0",
"react-markdown": "^9.0.1",
"rehype-raw": "^7.0.0",
Expand Down
13 changes: 13 additions & 0 deletions src/app/[locale]/stats/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
interface DataList {
kind: string
key: string
subkey: string | null
timestamps: number[]
values: number[]
}

interface GetDataResponse {
start: number
end: number
data: {[key: string]: DataList[]}
}
118 changes: 118 additions & 0 deletions src/app/[locale]/stats/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { LineChart } from "@/components/charts/line";
import { Counts, NestedCounts, PieChart } from "@/components/charts/pie";
import { CommonContentLayout } from "@/components/layout/common-content-layout";
import { TimeFormatted } from "@/components/time-formatted";
import { getTelemetryApiToken } from "@/utils/environment-utils";
import { pick } from "@/utils/i18n-utils";
import { getCountryCodeName } from "@/utils/iso-3166-utils";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages, getTranslations } from "next-intl/server";
import React from "react";

function getSecondsUntilNextHour(): number {
const now = new Date()
return 3600 - (now.getMinutes() * 60 + now.getSeconds())
}

async function mapCountryCode(counts: Counts): Promise<Counts> {
const newCounts: Counts = {}
for (const [code, value] of Object.entries(counts)) {
const newCode = await getCountryCodeName(code) || code
newCounts[newCode] = value
}
return newCounts
}

export default async function Page() {
const locale = await getLocale()
const messages = await getMessages()
const t = await getTranslations('page.stats')

const url = 'https://telemetry.mcdreforged.com/api/data'
const fetchRsp = await fetch(url, {
headers: {
'Authorization': `Bearer ${getTelemetryApiToken()}`,
},
next: {
revalidate: Math.min(10 * 60, getSecondsUntilNextHour() + 30),
tags: ['telemetry'],
},
})
if (fetchRsp.status !== 200) {
console.error(`fetch telemetry data failed: ${fetchRsp.status} ${fetchRsp.statusText}`, fetchRsp)
return <div>Telemetry data fetching failed</div>
}
const rsp = (await fetchRsp.json()) as GetDataResponse

const mcdrInstancesTimstamps = rsp.data['mcdr_instance'][0].timestamps
const mcdrInstancesValues: {[label: string]: number[]} = {}
mcdrInstancesValues[t('line.total')] = rsp.data['mcdr_instance'][0].values
rsp.data['mcdr_version'].forEach(dataList => {
if (dataList.subkey) {
const points = new Map(dataList.timestamps.map((ts, index) => [ts, dataList.values[index]]))
mcdrInstancesValues[dataList.subkey] = mcdrInstancesTimstamps.map(ts => (points.get(ts) || 0))
}
})

function extractCount(kind: string): NestedCounts {
let counts: NestedCounts = {
keyCounts: {},
subkeyCounts: {},
}
rsp.data[kind].forEach(dataList => {
const key = dataList.key
const subkey = dataList.subkey
if (!key) {
return
}
if (dataList.timestamps[dataList.timestamps.length - 1] !== rsp.end) {
return
}
const value = dataList.values[dataList.values.length - 1]
counts.keyCounts[key] = (counts.keyCounts[key] || 0) + value
if (subkey) {
if (!counts.subkeyCounts[key]) {
counts.subkeyCounts[key] = {}
}
counts.subkeyCounts[key][subkey] = value
}
})
return counts
}

// XXX: debug only
function HourLine({label, timestamp}: {label: string; timestamp: number}) {
return (
<p>
<span>{label}</span>
<TimeFormatted date={new Date(timestamp)} format="LLL" hoverOpenDelay={500} component="span"/>
</p>
)
}

const countryCounts = extractCount('country')
countryCounts.keyCounts = await mapCountryCode(countryCounts.keyCounts)

return (
<CommonContentLayout>
<NextIntlClientProvider locale={locale} messages={pick(messages, 'page.stats')}>
<div>
<HourLine label="Start: " timestamp={rsp.start * 1000}/>
<HourLine label="End: " timestamp={rsp.end * 1000}/>
<p>Hour count: {(rsp.end - rsp.start) / 3600 + 1}</p>
</div>
<div className="flex flex-col gap-10">
<LineChart title={t('kind.mcdr_instance')} timestamps={mcdrInstancesTimstamps} values={mcdrInstancesValues}/>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 flex-wrap lg:mx-20">
<PieChart title={t('kind.mcdr_version')} counts={extractCount('mcdr_version')}/>
<PieChart title={t('kind.python_version')} counts={extractCount('python_version')}/>
<PieChart title={t('kind.system_version')} counts={extractCount('system_version')}/>
<PieChart title={t('kind.system_arch')} counts={extractCount('system_arch')}/>
<PieChart title={t('kind.deployment_method')} counts={extractCount('deployment_method')}/>
<PieChart title={t('kind.country')} counts={countryCounts}/>
</div>
</div>
</NextIntlClientProvider>
</CommonContentLayout>
)
}
3 changes: 2 additions & 1 deletion src/app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { getRevalidateCatalogueToken } from "@/utils/environment-utils";
import { revalidatePath, revalidateTag } from "next/cache";
import { NextRequest } from "next/server";

const revalidateToken = process.env.MW_REVALIDATE_CATALOGUE_TOKEN
const revalidateToken = getRevalidateCatalogueToken()

export async function POST(request: NextRequest) {
if (revalidateToken === undefined) {
Expand Down
5 changes: 3 additions & 2 deletions src/catalogue/data.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AllOfAPlugin, Everything } from "@/catalogue/meta-types";
import { SimpleEverything } from "@/catalogue/simple-types";
import { getCatalogueEverythingUrl, shouldReadCatalogueEverythingFromLocalFile } from "@/utils/environment-utils";
import fs from 'fs/promises'
import { notFound } from "next/navigation";
import { promisify } from "node:util";
Expand All @@ -17,7 +18,7 @@ async function fileExists(filePath: string) {
}

async function devReadLocalEverything(): Promise<Everything | null> {
if (process.env.MW_USE_LOCAL_EVERYTHING === 'true') {
if (shouldReadCatalogueEverythingFromLocalFile()) {
const localDataPath = path.join(process.cwd(), 'src', 'catalogue', 'everything.json')
if (await fileExists(localDataPath)) {
const content = await fs.readFile(localDataPath, 'utf8')
Expand All @@ -30,7 +31,7 @@ async function devReadLocalEverything(): Promise<Everything | null> {
const gunzipAsync = promisify(gunzip)

async function fetchEverything(): Promise<Everything> {
const url: string = process.env.MW_EVERYTHING_JSON_URL || 'https://raw.githubusercontent.com/MCDReforged/PluginCatalogue/meta/everything.json.gz'
const url: string = getCatalogueEverythingUrl()

// The 2nd init param cannot be defined as a standalone global constant variable,
// or the ISR might be broken: fetchEverything() will never be invoked after the first 2 round of requests,
Expand Down
Loading

0 comments on commit 936c982

Please sign in to comment.