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
10 changes: 5 additions & 5 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,11 @@ Please include:

## Version Support

| Version | Status | Support Until |
|---------|--------|---------------|
| 3.2.x | Current | TBD |
| 3.1.x | Security Only | TBD |
| < 3.1 | Unsupported | - |
| Version | Status | Support Until |
|---------|---------------|---------------|
| 3.7.x | Current | 3.8.x Release |
| 3.6.x | Security Only | ~ Aug 2026 |
| < 3.5 | Unsupported | End of life |
Comment on lines +110 to +114

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Version table leaves 3.5.x support status undefined.

< 3.5 excludes 3.5.x itself, and 3.6.x doesn't cover it either — so the version this PR is bumping from (3.5.4, per package.json) has no documented support status. Also worth tightening the inconsistent "Support Until" phrasing (3.8.x Release vs ~ Aug 2026 vs End of life).

📝 Proposed fix
 | Version | Status        | Support Until |
 |---------|---------------|---------------|
 | 3.7.x   | Current       | 3.8.x Release |
 | 3.6.x   | Security Only | Aug 2026      |
-| < 3.5   | Unsupported   | End of life   |
+| 3.5.x   | Unsupported   | End of life   |
+| < 3.5   | Unsupported   | End of life   |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Version | Status | Support Until |
|---------|---------------|---------------|
| 3.7.x | Current | 3.8.x Release |
| 3.6.x | Security Only | ~ Aug 2026 |
| < 3.5 | Unsupported | End of life |
| Version | Status | Support Until |
|---------|---------------|---------------|
| 3.7.x | Current | 3.8.x Release |
| 3.6.x | Security Only | Aug 2026 |
| 3.5.x | Unsupported | End of life |
| < 3.5 | Unsupported | End of life |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/SECURITY.md around lines 110 - 114, Update the version-support table
in SECURITY.md to explicitly classify 3.5.x, including the current baseline
version 3.5.4, and ensure the version ranges do not leave any release uncovered.
Normalize the “Support Until” wording across all rows, preserving the intended
support dates and unsupported status.


**Note**: Versions older than the current major version may have limited security support.

Expand Down
61 changes: 61 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,67 @@ All notable changes to the NodeByte Hosting website will be documented in this f
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.7.0] - 2026-07-13

### Added

- **Unified Game Server Hosting** — `/games` now shows one flat plan grid for every supported game instead of a separate curated page per game; game selection moves to a Paymenter checkout config option
- `packages/ui/components/Layouts/Games/game-hub.tsx` — new hub component (search, sort, plan grid) matching the VPS/Dedicated hub pattern
- `app/games/page.tsx` — rewritten to flatten `getGamePlans()` across every child of the Game Servers hub instead of rendering one curated card per category
- `GamePlanSpec.category` field added (which Paymenter category a plan came from, for search/de-dup — not used for grouping)
- `packages/core/constants/supported-games.ts` — manually maintained `SUPPORTED_GAMES` list shown as badges on the `/games` hero; there's no admin API to read Paymenter's config-option choices live, so this is hand-kept in sync with the checkout dropdown
- Removed: `/games/[slug]` dynamic route, `packages/core/products/catalog-config.ts`, `packages/core/constants/game/*` curated per-game content, and the 4 now-unused `Games` layout components (`game-hero`, `game-features`, `game-pricing`, `game-faq`)

- **Partners & Sponsors Page** — new `/partners` page
- `packages/core/constants/partners.ts` — maintainable data file; entries have a `sponsor` / `partner` / `community` tier and an optional `category` badge (e.g. "Minecraft Server", "FiveM Community")
- `packages/ui/components/Layouts/Partners/partners-page.tsx` — hero, one section per tier (a tier with zero entries doesn't render at all rather than showing an empty state), CTA linking to the existing KB partner program docs
- Nav "Company" dropdown, footer "Company" column, and `sitemap.ts` all link to `/partners`

- **Expandable "Server Info" panel** on every Game/VPS/Dedicated plan card — a "View Server Info" toggle reveals Setup Fee, CPU Model, Uplink, DDoS layers, Location, Databases, and Backups, all parsed live from the Paymenter product description
- `packages/ui/components/ui/plan-info-row.tsx` — shared label/value row, built on the existing Radix `Collapsible` primitive
- `getSetupFeeGBP` / `getSetupFeesMap` (`bytepay.ts`) — one-time setup fee per plan, mirroring the existing price helpers
- `location` / `databases` / `backups` extraction (`spec-parser.ts`) — e.g. "3x MySQL Databases", "Newcastle, UK or New York, US", "Automatic Daily Backups"

- **VPS/Dedicated Hub — richer filtering & sorting**
- RAM tier filter chips, built live from the RAM values actually present in the plan list (reliable regardless of naming convention)
- Price range (min/max) filter
- Sort extended from price-only to price / RAM / storage / CPU cores, each ascending or descending; default sort is now price ascending on Games, VPS, and Dedicated
- `packages/ui/components/ui/filter-chip.tsx` — shared `FilterChipRow` / `FilterChip`, replacing duplicated chip markup in both hubs
- Filter bar redesigned: Search + Sort + a "Filters" toggle (with an active-filter-count badge) in one compact row; the actual filter chips live in a collapsible panel grouped into labeled rows (Price / RAM / Hardware / Lineup / Series) instead of one long wrapped cluster — considerably shorter on mobile

- **Live node discovery** — `/nodes` now reads its node list from status.nodebyte.host's "Nodes" monitor group instead of a hardcoded array, so adding a node to the status page is enough for it to appear on the site
- `getNodeMonitorNames()` (`lib/status.ts`) — filters the status snapshot to the "Nodes" group, excluding aggregate "group"-type rollups
- `NODE_DISPLAY_OVERRIDES` (`constants/status-mapping.ts`) replaces `NODE_MONITOR_MAP` — a small optional table for location/hardware details the status API doesn't carry, keyed by monitor name

### Changed

- **Category hub discovery hardened** (`bytepay.ts`)
- `getCategoryHub` now falls back to substring matching (with a console warning) if no exact alias match is found, so a category rename in Paymenter degrades gracefully instead of silently emptying a whole section
- A hub always includes itself as a leaf alongside its real sub-categories, not only when it has none — fixes products filed directly on a hub (e.g. the new generic `GAME-BASE`/`GAME-PLUS` tiers, filed on "Game Servers" itself alongside its Minecraft/Rust/Hytale children) being invisible on the site
- `fetchWithRetry` — every billing panel request now has a 10s timeout and retries transient failures (429/5xx/network errors) with backoff, honouring `Retry-After`; `per_page` increased to reduce pagination round-trips; missing `BYTEPAY_HOST`/`BYTEPAY_TOKEN` now throws a clear error instead of building a URL like `undefined/api/...`

- **`spec-parser.ts` rewritten to classify description lines by topic instead of matching one exact phrase shape** — tolerates much more variation in how a product description is worded
- Fixed a real bug along the way: `bulletLines()` wasn't splitting on `<li>` boundaries, so line-based extraction was silently reading across bullet points in Paymenter's default `<ul><li>` description format
- CPU brand detection broadened: "Ryzen" no longer requires an "AMD" prefix, "Intel i9-9900K" no longer requires the word "Core"
- CPU core count now also catches "8 Dedicated/Pinned Physical Cores" (number not at the start of the bullet) via a bounded per-line pattern, without guessing wrong on CPU model numbers like "Ryzen™ 9 5900X"
- Storage detection catches "Local NVMe Storage" and similar phrasing lacking an explicit SSD/HDD/Disk keyword
- Location extraction handles both "Newcastle, UK" (short code) and "Falkenstein, Germany" (full country name) styles, triggered by a wider set of keywords (location/region/infrastructure/provisioned/hosted/data centre)

- **Nav Services dropdown simplified** to 3 flat links (Game Servers / VPS Servers / Dedicated Servers), replacing the old sectioned mega-menu with a per-game submenu; the now-dead `gamesNav` prop chain (`app/layout.tsx` → `layout-chrome.tsx` → `navigation.tsx`) was removed along with the billing-panel fetch it required on every page load

- **Dropped-product alerting** — `getGamePlans` / `getVpsPlans` / `getDedicatedPlans` now log a warning when a live, non-hidden product fails to parse its required specs (instead of silently vanishing from the site), and the dev-only `/api/billing-debug` route lists every such product with its missing fields

### Fixed

- **Plan card layout bug** — `h-full` on plan cards was silently fighting the grid's `items-start` sizing, clipping the "Order Now" button on a collapsed sibling card whenever another card's Server Info panel was expanded; removed, cards now size purely to their own content
- Card grids no longer visually stretch every card in a row to match the tallest (expanded) one — `items-start` added to the Game/VPS/Dedicated plan grids

### Security

- **Double-unescaping in `spec-parser.ts`'s HTML entity decoding** (CodeQL `js/double-escaping`) — `bulletLines()` unescaped `&amp;` → `&` *before* `&gt;`/`&lt;`/`&quot;`/`&#39;`, so a properly double-escaped literal like `&amp;gt;` (meant to render as the text `&gt;`) was silently corrupted into `>` instead. Fixed by unescaping `&amp;` last, matching the standard safe-unescaping order — the escape character itself must always be resolved after every other entity that depends on it.

---

## [3.6.0] - 2026-07-04

### Added
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@nodebyte/hosting-site",
"description": "The official website for NodeByte Hosting.",
"license": "AGPL-3.0-only",
"version": "3.5.4",
"version": "3.7.0",
"scripts": {
"build": "next build",
"dev": "next dev",
Expand Down
5 changes: 4 additions & 1 deletion packages/core/lib/spec-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,17 @@ function bulletLines(html: string): string[] {
const withBreaks = html
.replace(/<\/(li|p|div|h[1-6])>/gi, "\n")
.replace(/<br\s*\/?>/gi, "\n")
// Unescape "&amp;" last — it must never run before the other entities, or
// e.g. a literal "&amp;gt;" in the source (an escaped ampersand followed by
// "gt;") would double-unescape into ">" instead of the correct "&gt;".
const text = withBreaks
.replace(/<[^>]*>/g, " ")
.replace(/&amp;/g, "&")
.replace(/&gt;/g, ">")
.replace(/&lt;/g, "<")
.replace(/&quot;/g, '"')
.replace(/&#0?39;/g, "'")
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/[ \t]+/g, " ")
return text
.split(/[•\n]/)
Expand Down
144 changes: 70 additions & 74 deletions packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Network,
Shield,
Zap,
SlidersHorizontal,
X,
ArrowRight,
ChevronDown,
Expand All @@ -17,6 +18,7 @@ import {
PackageX,
} from "lucide-react"
import { Button } from "@/packages/ui/components/ui/button"
import { Badge } from "@/packages/ui/components/ui/badge"
import { Input } from "@/packages/ui/components/ui/input"
import {
Select,
Expand All @@ -27,6 +29,7 @@ import {
} from "@/packages/ui/components/ui/select"
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/packages/ui/components/ui/collapsible"
import { PlanInfoRow } from "@/packages/ui/components/ui/plan-info-row"
import { FilterChipRow, FilterChip } from "@/packages/ui/components/ui/filter-chip"
import Link from "next/link"
import { cn } from "@/lib/utils"
import type { DedicatedPlanSpec } from "@/packages/core/types/servers/dedicated"
Expand Down Expand Up @@ -220,7 +223,8 @@ export function DedicatedHub({ plans }: DedicatedHubProps) {
const [ram, setRam] = useState<number | "ALL">("ALL")
const [priceMin, setPriceMin] = useState("")
const [priceMax, setPriceMax] = useState("")
const [sort, setSort] = useState<SortKey>("default")
const [sort, setSort] = useState<SortKey>("price-asc")
const [filtersOpen, setFiltersOpen] = useState(false)

// Derived from live plan data, not the server name — reliable regardless of naming.
const availableRam = Array.from(new Set(plans.map((p) => p.ramGB))).sort((a, b) => a - b)
Expand Down Expand Up @@ -253,16 +257,18 @@ export function DedicatedHub({ plans }: DedicatedHubProps) {
return result
})()

const hasActiveFilters =
hardware !== "ALL" || ram !== "ALL" || priceMin !== "" || priceMax !== "" || search !== ""
const activeFilterCount = [
hardware !== "ALL", ram !== "ALL", priceMin !== "", priceMax !== "",
].filter(Boolean).length
const hasActiveFilters = activeFilterCount > 0 || search !== ""

function clearFilters() {
setSearch("")
setHardware("ALL")
setRam("ALL")
setPriceMin("")
setPriceMax("")
setSort("default")
setSort("price-asc")
}

return (
Expand Down Expand Up @@ -325,7 +331,7 @@ export function DedicatedHub({ plans }: DedicatedHubProps) {
/>
</div>
<Select value={sort} onValueChange={(v) => setSort(v as SortKey)}>
<SelectTrigger className="w-48 bg-card/30 border-border/50">
<SelectTrigger className="w-full sm:w-48 bg-card/30 border-border/50">
<SelectValue />
</SelectTrigger>
<SelectContent>
Expand All @@ -340,82 +346,72 @@ export function DedicatedHub({ plans }: DedicatedHubProps) {
<SelectItem value="cpu-desc">CPU Cores: High → Low</SelectItem>
</SelectContent>
</Select>
<div className="flex items-center gap-1.5">
<Input
type="number"
inputMode="decimal"
placeholder="Min £"
value={priceMin}
onChange={(e) => setPriceMin(e.target.value)}
className="w-24 bg-card/30 border-border/50"
/>
<span className="text-muted-foreground text-sm">–</span>
<Input
type="number"
inputMode="decimal"
placeholder="Max £"
value={priceMax}
onChange={(e) => setPriceMax(e.target.value)}
className="w-24 bg-card/30 border-border/50"
/>
</div>

{/* Hardware filter */}
<div className="flex gap-1.5">
{(["ALL", "amd", "intel"] as const).map((h) => (
<button
key={h}
type="button"
onClick={() => setHardware(h)}
className={cn(
"px-3 py-1.5 rounded-full text-xs font-medium border transition-all",
hardware === h
? "border-primary/50 bg-primary/10 text-primary"
: "border-border/50 text-muted-foreground hover:border-border hover:text-foreground",
)}
>
{h === "ALL" ? "All Hardware" : h.toUpperCase()}
</button>
))}
</div>

<Button
variant="outline"
size="sm"
onClick={() => setFiltersOpen((v) => !v)}
className="gap-1.5 bg-card/30 border-border/50"
>
<SlidersHorizontal className="w-3.5 h-3.5" />
Filters
{activeFilterCount > 0 && (
<Badge variant="secondary" className="ml-0.5 h-4 min-w-4 px-1 text-[10px]">
{activeFilterCount}
</Badge>
)}
<ChevronDown className={cn("w-3.5 h-3.5 transition-transform", filtersOpen && "rotate-180")} />
</Button>
{hasActiveFilters && (
<Button variant="ghost" size="sm" onClick={clearFilters} className="gap-1.5 text-muted-foreground">
<X className="w-3.5 h-3.5" /> Clear
</Button>
)}
</div>

{/* RAM tier row */}
<div className="flex flex-wrap gap-1.5 items-center">
<button
type="button"
onClick={() => setRam("ALL")}
className={cn(
"px-3 py-1 rounded-full text-xs font-medium border transition-all",
ram === "ALL"
? "border-primary/50 bg-primary/10 text-primary"
: "border-border/50 text-muted-foreground hover:border-border hover:text-foreground",
)}
>
All RAM
</button>
{availableRam.map((r) => (
<button
key={r}
type="button"
onClick={() => setRam(ram === r ? "ALL" : r)}
className={cn(
"px-3 py-1 rounded-full text-xs font-medium border transition-all",
ram === r
? "border-primary/50 bg-primary/10 text-primary"
: "border-border/50 text-muted-foreground hover:border-border hover:text-foreground",
)}
>
{r} GB
</button>
))}
</div>
<Collapsible open={filtersOpen} onOpenChange={setFiltersOpen}>
<CollapsibleContent>
<div className="rounded-xl border border-border/50 bg-card/20 p-4 space-y-3">
<FilterChipRow label="Price">
<div className="flex items-center gap-1.5">
<Input
type="number"
inputMode="decimal"
placeholder="Min £"
value={priceMin}
onChange={(e) => setPriceMin(e.target.value)}
className="w-24 h-7 bg-card/30 border-border/50 text-xs"
/>
<span className="text-muted-foreground text-sm">–</span>
<Input
type="number"
inputMode="decimal"
placeholder="Max £"
value={priceMax}
onChange={(e) => setPriceMax(e.target.value)}
className="w-24 h-7 bg-card/30 border-border/50 text-xs"
/>
</div>
</FilterChipRow>

<FilterChipRow label="RAM">
<FilterChip active={ram === "ALL"} onClick={() => setRam("ALL")}>All RAM</FilterChip>
{availableRam.map((r) => (
<FilterChip key={r} active={ram === r} onClick={() => setRam(ram === r ? "ALL" : r)}>
{r} GB
</FilterChip>
))}
</FilterChipRow>

<FilterChipRow label="Hardware">
{(["ALL", "amd", "intel"] as const).map((h) => (
<FilterChip key={h} active={hardware === h} onClick={() => setHardware(h)}>
{h === "ALL" ? "All Hardware" : h.toUpperCase()}
</FilterChip>
))}
</FilterChipRow>
</div>
</CollapsibleContent>
</Collapsible>
Comment on lines +349 to +414

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== dedicated-hub.tsx relevant lines ==\n'
sed -n '320,440p' packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx

printf '\n== vps-hub.tsx relevant lines ==\n'
sed -n '340,430p' packages/ui/components/Layouts/VPS/vps-hub.tsx

printf '\n== search for CollapsibleTrigger / aria-expanded / aria-controls in related UI ==\n'
rg -n "CollapsibleTrigger|aria-expanded|aria-controls|CollapsibleContent" packages/ui/components/Layouts packages/ui/components -g '*.tsx' | sed -n '1,200p'

Repository: NodeByteHosting/website

Length of output: 15968


Add disclosure ARIA to the Filters toggle in both hubs.

The current plain Button only toggles filtersOpen; it doesn’t expose aria-expanded/aria-controls for the filter panel. Use CollapsibleTrigger asChild or add those attributes plus a matching panel id in:

  • packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx#L349-L414
  • packages/ui/components/Layouts/VPS/vps-hub.tsx#L377-L397
📍 Affects 2 files
  • packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx#L349-L414 (this comment)
  • packages/ui/components/Layouts/VPS/vps-hub.tsx#L377-L397
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx` around lines 349
- 414, Update the Filters toggle and filter panel in
packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx#L349-L414 and
packages/ui/components/Layouts/VPS/vps-hub.tsx#L377-L397: associate each Button
with its Collapsible using CollapsibleTrigger asChild or matching aria-expanded
and aria-controls attributes, and assign a corresponding id to each panel. Keep
the existing filtersOpen toggle behavior intact.

</div>

{/* ── Plan grid ────────────────────────────────────────────────────── */}
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/components/Layouts/Games/game-hub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ interface GameHubProps {

export function GameHub({ plans }: GameHubProps) {
const [search, setSearch] = useState("")
const [sort, setSort] = useState<SortKey>("default")
const [sort, setSort] = useState<SortKey>("asc")

const filtered = (() => {
let result = [...plans]
Expand All @@ -198,7 +198,7 @@ export function GameHub({ plans }: GameHubProps) {

function clearFilters() {
setSearch("")
setSort("default")
setSort("asc")
}

return (
Expand Down
Loading