Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ JWT_SECRET=""

# DeepL API key for automated translation (https://www.deepl.com/pro-api)
# Free tier: use a key ending in :fx | Paid tier: standard key
DEEPL_API_KEY=""
DEEPL_API_KEY=""

# status.nodebyte.host public status API — admin token unlocks hidden monitors
# and bypasses the shared CDN cache for fresher data.
STATUS_API_URL="https://status.nodebyte.host"
STATUS_TOKEN=""
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ The frontend uses a centralized API client (`packages/core/lib/api.ts`) that rou

A small set of lightweight API routes remain in the Next.js app for public-facing proxy endpoints:
- `/api/github/releases` -- GitHub release data
- `/api/instatus` -- Status page integration
- `/api/status` -- status.nodebyte.host integration (node/service monitor status, uptime, latency)
- `/api/panel/*` -- Public panel data (counts, nodes, servers, stats, users)
- `/api/trustpilot` -- Trustpilot review data

Expand All @@ -142,7 +142,7 @@ A small set of lightweight API routes remain in the Next.js app for public-facin
│ │ └── users/ # User management
│ ├── api/ # Lightweight proxy routes
│ │ ├── github/releases/ # GitHub releases proxy
│ │ ├── instatus/ # Status page proxy
│ │ ├── status/ # status.nodebyte.host proxy
│ │ ├── panel/ # Public panel data
│ │ └── trustpilot/ # Trustpilot proxy
│ ├── auth/ # Authentication pages
Expand Down
18 changes: 17 additions & 1 deletion app/api/billing-debug/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fetchAllBillingProducts } from "@/packages/core/lib/bytepay"
import { parseDescriptionSpecs } from "@/packages/core/lib/spec-parser"

export const dynamic = "force-dynamic"

Expand Down Expand Up @@ -27,5 +28,20 @@ export async function GET(request: Request) {
description: p.description,
}))

return Response.json({ count: summary.length, products: summary })
// Live products whose description fails to parse cpu/ramGB/storageGB — a
// subset (or all) of these fields are required by getGamePlans/getVpsPlans/
// getDedicatedPlans, so a missing one here means the product silently
// disappears from its billing page.
const dropped = filtered.flatMap((p) => {
const parsed = parseDescriptionSpecs(p.description)
const missing = [
!parsed.cpu && "cpu",
!parsed.ramGB && "ramGB",
!parsed.storageGB && "storageGB",
].filter((v): v is string => Boolean(v))
if (missing.length === 0) return []
return [{ id: p.id, name: p.name, slug: p.slug, categorySlug: p.categorySlug, missing }]
})

return Response.json({ count: summary.length, products: summary, dropped })
}
92 changes: 0 additions & 92 deletions app/api/instatus/route.ts

This file was deleted.

52 changes: 52 additions & 0 deletions app/api/status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { NextResponse } from "next/server"
import { computeLatencyStats, fetchStatusSnapshot, type MonitorStatus, type MonitorType } from "@/packages/core/lib/status"

export const revalidate = 30

export interface StatusApiMonitor {
name: string
type: MonitorType
groupName: string | null
subgroupName: string | null
status: MonitorStatus
uptime30dPct: number | null
latency: { fast: number; avg: number; slow: number } | null
}

export interface StatusApiResponse {
available: boolean
generatedAt: number | null
overallStatus: MonitorStatus | null
monitors: StatusApiMonitor[]
}

export async function GET() {
const snapshot = await fetchStatusSnapshot()

if (!snapshot) {
return NextResponse.json<StatusApiResponse>(
{ available: false, generatedAt: null, overallStatus: null, monitors: [] },
{ status: 200 },
)
}

const monitors: StatusApiMonitor[] = snapshot.monitors.map((m) => ({
name: m.name,
type: m.type,
groupName: m.group_name,
subgroupName: m.subgroup_name,
status: m.status,
uptime30dPct: m.uptime_30d_pct,
latency: computeLatencyStats(m),
}))

return NextResponse.json<StatusApiResponse>(
{ available: true, generatedAt: snapshot.generated_at, overallStatus: snapshot.overall_status, monitors },
{
status: 200,
headers: {
"Cache-Control": "public, s-maxage=30, stale-while-revalidate=60",
},
},
)
}
21 changes: 20 additions & 1 deletion packages/core/constants/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,26 @@ export const SERVICE_CATEGORIES: ServiceCategory[] = [
highlights: [
"Enterprise-grade processors",
"Full root / SSH access",
"NVMe SSD storage",
"Lightning fast networks",
"Enterprise DDoS protection",
],
enabled: true,
},
{
id: "dedicated",
name: "Dedicated Servers",
description:
"Physical bare-metal servers with fully dedicated CPU cores, enterprise storage, and IPMI out-of-band access. Zero resource contention and maximum raw performance.",
href: "/dedicated",
icon: Cpu,
gradient: "from-amber-600/25 via-amber-500/8 to-transparent",
iconColor: "text-amber-400",
accentBorder: "hover:border-amber-400/40",
startingPriceGBP: 50,
highlights: [
"100% dedicated CPU cores",
"IPMI out-of-band access",
"Enterprise storage",
"Enterprise DDoS protection",
],
enabled: true,
Expand Down
30 changes: 30 additions & 0 deletions packages/core/constants/status-mapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Bridges the two independent naming schemes between status.nodebyte.host
* (monitor names) and this site's node/location identifiers. Update these
* maps whenever a node is renamed or a new Region ping monitor is added
* upstream — unmapped entries simply render without live data.
*/

/** Website node `name` (STATIC_NODES) → status.nodebyte.host monitor `name`. */
export const NODE_MONITOR_MAP: Record<string, string> = {
"NEWC-GAME1": "NEWC-GAME1",
"HEL-VPS1": "HEL-VPS1",
}

/**
* Website location `id` (LOCATIONS) → status.nodebyte.host "Regions" monitor
* `name`. Only locations with a confirmed matching ping monitor are listed;
* the Regions group also includes PoPs (e.g. Ashburn VA, Atlanta GA) that
* don't correspond to an actual NodeByte data centre location.
*/
export const LOCATION_MONITOR_MAP: Record<string, string> = {
lon: "London, UK",
fal: "Falkenstein, DE",
fra: "Frankfurt, DE",
hel: "Helsinki, FI",
tor: "Toronto, ON",
vhv: "Ashburn, VA", // Vint Hill, VA is in the same Northern Virginia / DC-metro area
sgp: "Singapore, Singapore",
syd: "Sydney, Australia",
mum: "Mumbai, India",
}
41 changes: 41 additions & 0 deletions packages/core/hooks/use-node-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"use client"

import { useEffect, useState } from "react"
import type { StatusApiMonitor, StatusApiResponse } from "@/app/api/status/route"
import type { MonitorStatus } from "@/packages/core/lib/status"

const REFRESH_INTERVAL_MS = 60_000

export function useNodeStatus() {
const [monitors, setMonitors] = useState<StatusApiMonitor[]>([])
const [available, setAvailable] = useState(false)
const [overallStatus, setOverallStatus] = useState<MonitorStatus | null>(null)

useEffect(() => {
let cancelled = false

const load = () => {
fetch("/api/status")
.then((r) => r.json())
.then((data: StatusApiResponse) => {
if (cancelled) return
setAvailable(data.available)
setMonitors(data.monitors)
setOverallStatus(data.overallStatus)
})
.catch(() => {})
}

load()
const interval = setInterval(load, REFRESH_INTERVAL_MS)
return () => {
cancelled = true
clearInterval(interval)
}
}, [])

const findMonitor = (name: string) =>
monitors.find((m) => m.name.trim().toLowerCase() === name.trim().toLowerCase()) ?? null

return { available, overallStatus, monitors, findMonitor }
}
Loading