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: 5 additions & 2 deletions app/nodes/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import type { Metadata } from "next"
import { NodesClient } from "@/packages/ui/components/Layouts/Nodes/nodes-client"
import { fetchStatusSnapshot, getNodeMonitorNames } from "@/packages/core/lib/status"

export const metadata: Metadata = {
title: "Our Network & Nodes",
description:
"Live status and information about NodeByte Hosting's network. See where our infrastructure is located and what powers our game server and VPS hosting.",
}

export default function NodesPage() {
return <NodesClient />
export default async function NodesPage() {
const snapshot = await fetchStatusSnapshot()
const nodeNames = getNodeMonitorNames(snapshot)
return <NodesClient nodeNames={nodeNames} />
}
8 changes: 4 additions & 4 deletions packages/core/constants/node-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ export interface PublicNode {
name: string
locationCode: string
isMaintenanceMode: boolean
/** Allocated memory in MiB */
memory: number
/** Allocated disk in MiB */
disk: number
/** Allocated memory in MiB — not known for nodes without a display override. */
memory?: number
/** Allocated disk in MiB — not known for nodes without a display override. */
disk?: number
/** Number of server instances currently provisioned on this node */
serverCount?: number
}
69 changes: 54 additions & 15 deletions packages/core/constants/partners.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,89 @@
/**
* Partners & sponsors shown on /partners.
* Partners, sponsors, and communities shown on /partners.
*
* Add or remove entries here — the page groups them by `tier` automatically
* and shows an empty-state invite for any tier with zero entries, so this
* file can be edited freely without touching the page component.
* Add or remove entries here — the page groups them by `tier` into their own
* section automatically, hiding any tier with zero entries, so this file can
* be edited freely without touching the page component.
*
* There's no real partner/sponsor data yet — the entries below are clearly
* marked placeholders. Replace them (or delete down to an empty array) once
* real partners sign on.
* - "sponsor" / "partner": companies and projects we have a business
* relationship with.
* - "community": servers/communities we host or sponsor for free or at a
* discount (Minecraft servers, Discord communities, etc). Set `category`
* to a short label like "Minecraft Server" or "Discord Community" — it
* renders as a badge on the card.
*/

export type PartnerTier = "sponsor" | "partner"
export type PartnerTier = "sponsor" | "partner" | "community"

export interface PartnerEntry {
id: string
name: string
tier: PartnerTier
/** Short badge label, e.g. "Minecraft Server", "Discord Community". Optional — mainly useful for the community tier. */
category?: string
/** Path under /public, e.g. "/partners/example.svg" */
logo: string
url: string
description: string
}

export const PARTNERS: PartnerEntry[] = [
{
id: "poliberry",
name: "Poliberry",
tier: "partner",
logo: "/partners/poliberry.png",
url: "https://poliberry.com",
description: "Poliberry is a technology company building tools and services to better connect people online and empowering developers to build the next big thing.",
},
{
id: "embrly",
name: "Emberly",
tier: "partner",
logo: "https://embrly.ca/icon.svg",
logo: "/partners/emberly.svg",
url: "https://embrly.ca",
description: "The open-source platform for secure file sharing and team collaboration. Upload, manage, and share content with custom domains, rich embeds, and built-in talent discovery.",
},
{
id: "clovrme",
name: "Clover",
tier: "partner",
logo: "/partners/clovrme.svg",
url: "https://clovr.me",
description: "A profile page that's fully yours custom themes, music, animated backgrounds, and all your links in one place.",
},
{
id: "octo",
name: "Octoflow",
tier: "partner",
logo: "https://octoflow.ca/logo.png",
logo: "/partners/octoflow.png",
url: "https://octoflow.ca",
description: "Keep your team connected to every commit, pull request, and deployment without ever leaving your Discord server.",
},
{
id: "poliberry",
name: "Poliberry",
id: "antiraid",
name: "AntiRaid",
tier: "partner",
logo: "/partners/Frame_1552.png",
url: "https://poliberry.com",
description: "Poliberry is a technology company building tools and services to better connect people online and empowering developers to build the next big thing.",
logo: "/partners/antiraid.webp",
url: "https://antiraid.xyz",
description: "From basic moderation to advanced threat protection, AntiRaid handles it all so you can focus on growing your community.",
},
{
id: "smphub",
name: "SMP Hub",
tier: "community",
category: "Minecraft Community",
logo: "/partners/smphub.webp",
url: "https://discord.gg/d6sXpA7gXJ",
description: "The premier directory designed to connect the Minecraft community.",
},
{
id: "blizzardsmp",
name: "Blizzard SMP",
tier: "community",
category: "Minecraft Community",
logo: "/partners/blizzardsmp.webp",
url: "https://discord.gg/mvQ9VqZ4D",
description: "A warm, active community with a cool name and even cooler players.",
},
]
28 changes: 16 additions & 12 deletions packages/core/constants/status-mapping.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
/**
* 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.
* The node list on /nodes is discovered live from status.nodebyte.host's
* "Nodes" group (see getNodeMonitorNames in lib/status.ts) — adding a node
* there is all that's needed for it to appear on the site.
*
* status.nodebyte.host doesn't carry location/hardware details, so those are
* filled in here as optional per-node overrides, keyed by the exact monitor
* name. A node with no entry here still shows up, just without these extras.
*/

/** Website node `name` (STATIC_NODES) → status.nodebyte.host monitor `name`. */
export const NODE_MONITOR_MAP: Record<string, string> = {
"NEWC-GAME1": "NEWC-GAME1",
"NEWY-GAME1": "NEWY-GAME1",
"FLUXRP-FIVEM": "FLUXRP-FIVEM",
"HEL-VPS1": "HEL-VPS1",
export const NODE_DISPLAY_OVERRIDES: Record<string, { locationCode?: string; cpu?: string; ramType?: string }> = {
"NEWC-GAME1": { locationCode: "Newcastle, UK" },
"NEWY-GAME1": { locationCode: "New York, USA" },
"HEL-VPS1": { locationCode: "Helsinki, FI" },
// Inferred from the "FSN" prefix (Falkenstein) — confirm/correct if wrong.
"FSN-VPS1": { locationCode: "Falkenstein, DE" },
}

/**
* 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.
* don't correspond to an actual NodeByte data centre location. Update
* whenever a new Region ping monitor is added upstream — unmapped locations
* simply render without live data.
*/
export const LOCATION_MONITOR_MAP: Record<string, string> = {
lon: "London, UK",
Expand Down
15 changes: 15 additions & 0 deletions packages/core/lib/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ export function findMonitor(snapshot: StatusSnapshot | null, name: string): Stat
return snapshot.monitors.find((m) => m.name.trim().toLowerCase() === target) ?? null
}

/**
* Names of individual node monitors under the "Nodes" status group — this is
* the live source of truth for which nodes exist on /nodes. Excludes the
* "group"-type aggregate rollups (e.g. "Game Servers", "VPS Servers") that
* summarise the individual node monitors rather than representing one.
* Add a node on status.nodebyte.host under the "Nodes" group and it appears
* here automatically — no website code change needed.
*/
export function getNodeMonitorNames(snapshot: StatusSnapshot | null): string[] {
if (!snapshot) return []
return snapshot.monitors
.filter((m) => m.group_name === "Nodes" && m.type !== "group")
.map((m) => m.name)
}

/** Compute fast/avg/slow latency (ms) from a monitor's recent heartbeats. */
export function computeLatencyStats(monitor: StatusMonitor): { fast: number; avg: number; slow: number } | null {
const samples = monitor.heartbeats
Expand Down
77 changes: 36 additions & 41 deletions packages/ui/components/Layouts/Nodes/nodes-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from "@/packages/ui/components/ui/accordion"
import { Alert, AlertDescription } from "@/packages/ui/components/ui/alert"
import type { PublicNode } from "@/packages/core/constants/node-types"
import { NODE_MONITOR_MAP, LOCATION_MONITOR_MAP } from "@/packages/core/constants/status-mapping"
import { NODE_DISPLAY_OVERRIDES, LOCATION_MONITOR_MAP } from "@/packages/core/constants/status-mapping"
import { useNodeStatus } from "@/packages/core/hooks/use-node-status"
import type { StatusApiMonitor } from "@/app/api/status/route"
import { cn } from "@/lib/utils"
Expand All @@ -36,35 +36,20 @@ interface ExtendedNode extends PublicNode {
uptime?: number
}

const STATIC_NODES: ExtendedNode[] = [
{
id: 1,
name: "NEWC-GAME1",
locationCode: "Newcastle, UK",
isMaintenanceMode: false,
memory: 1310089,
disk: 1811000,
uptime: 99.9,
},
{
id: 2,
name: "NEWY-GAME1",
locationCode: "New York, USA",
isMaintenanceMode: false,
memory: 65104,
disk: 512000,
uptime: 99.8,
},
{
id: 3,
name: "HEL-VPS1",
locationCode: "Helsinki, FI",
isMaintenanceMode: false,
memory: 65104,
disk: 512000,
uptime: 99.8,
},
]
/** Build the node list from names discovered live from the status page, filling in any known display extras. */
function buildNodes(nodeNames: string[]): ExtendedNode[] {
return nodeNames.map((name, i) => {
const overrides = NODE_DISPLAY_OVERRIDES[name] ?? {}
return {
id: i + 1,
name,
locationCode: overrides.locationCode ?? "",
isMaintenanceMode: false,
cpu: overrides.cpu,
ramType: overrides.ramType,
}
})
}

// ─── Location data ───────────────────────────────────────────────────────────

Expand Down Expand Up @@ -314,13 +299,15 @@ const FAQS = [

// ─── Main export ─────────────────────────────────────────────────────────────

export function NodesClient() {
const nodes = STATIC_NODES
interface NodesClientProps {
/** Node monitor names discovered live from the status page's "Nodes" group. */
nodeNames: string[]
}

export function NodesClient({ nodeNames }: NodesClientProps) {
const nodes = buildNodes(nodeNames)
const { findMonitor } = useNodeStatus()
const nodeLiveStates = nodes.map((node) => {
const monitorName = NODE_MONITOR_MAP[node.name]
return monitorName ? findMonitor(monitorName) : null
})
const nodeLiveStates = nodes.map((node) => findMonitor(node.name))
const onlineCount = nodeLiveStates.filter(
(live, i) => (live ? live.status === "up" : !nodes[i].isMaintenanceMode),
).length
Expand Down Expand Up @@ -381,11 +368,19 @@ export function NodesClient() {
</p>
<div className="h-px flex-1 bg-border/40" />
</div>
<div className="grid sm:grid-cols-2 gap-6 max-w-2xl mx-auto">
{nodes.map((node, i) => (
<NodeCard key={node.id} node={node} live={nodeLiveStates[i]} />
))}
</div>
{nodes.length > 0 ? (
<div className="grid sm:grid-cols-2 gap-6 max-w-2xl mx-auto">
{nodes.map((node, i) => (
<NodeCard key={node.id} node={node} live={nodeLiveStates[i]} />
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-16 text-center space-y-2 border border-border/40 rounded-2xl bg-card/20 max-w-2xl mx-auto">
<Server className="w-8 h-8 text-muted-foreground/40" />
<p className="font-medium">Node status is temporarily unavailable</p>
<p className="text-sm text-muted-foreground">Check the status page directly for the latest info.</p>
</div>
)}
</section>

{/* ── Locations */}
Expand Down
Loading