Skip to content

Development#123

Merged
CodeMeAPixel merged 5 commits into
productionfrom
development
Jul 6, 2026
Merged

Development#123
CodeMeAPixel merged 5 commits into
productionfrom
development

Conversation

@CodeMeAPixel

@CodeMeAPixel CodeMeAPixel commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added live system status support across the app, including a new status badge and updated status page links.
    • Introduced live node monitoring on the Nodes page with real-time status, uptime, and latency indicators.
    • Added billing-backed plan loading for game, dedicated, and VPS offerings.
  • Bug Fixes

    • Improved status handling and fallbacks when live data is unavailable.
    • Updated service and footer link layouts to better match the current site structure.
  • Documentation

    • Refreshed setup and API docs to reflect the new status endpoint and environment settings.

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
website Ready Ready Preview, Comment Jul 6, 2026 8:39am

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e258fbeb-5f1b-47fc-9c3f-93146ebb2c0e

📥 Commits

Reviewing files that changed from the base of the PR and between 3fcc57e and 24247b9.

📒 Files selected for processing (1)
  • packages/core/constants/services.ts

📝 Walkthrough

Walkthrough

This PR adds a Bytepay/Paymenter-backed billing product pipeline (fetching, JSON:API normalization, spec parsing, category plan builders, and a debug endpoint), replaces the instatus status proxy with a new status.nodebyte.host-backed API route, hook, and badge wired into Nodes/footer, plus minor services grid styling and a translations submodule bump.

Changes

Billing-native pricing and product catalog

Layer / File(s) Summary
Description/name spec parser
packages/core/lib/spec-parser.ts
Extracts CPU, RAM, storage, bandwidth, uplink, hardware, and SKU/lineup/series fields from HTML descriptions and product names via regex heuristics.
Bytepay API client, normalization, and helpers
packages/core/lib/bytepay.ts
Fetches Paymenter admin API data with retries, normalizes JSON:API resources into BillingProduct/Plan/Price types, maps categories to slugs, and exposes pricing/stock/URL helpers.
Category plan builders and debug endpoint
packages/core/products/billing-service.ts, app/api/billing-debug/route.ts
Builds cached game/dedicated/vps plan specs from parsed billing products, warns on dropped products, and exposes a development-only debug route listing parsed/dropped products.
Service category highlight/icon update
packages/core/constants/services.ts
Adds the Cpu icon import and updates VPS highlights alongside the dedicated category definition.

Estimated code review effort: 4 (Complex) | ~60 minutes

Live node status monitoring replacing instatus proxy

Layer / File(s) Summary
Status snapshot library and mapping constants
packages/core/lib/status.ts, packages/core/constants/status-mapping.ts
Fetches and normalizes status.nodebyte.host snapshots with monitor/heartbeat typing and latency helpers, plus node/location-to-monitor name mappings.
New /api/status route
app/api/status/route.ts
Exposes a GET route returning StatusApiMonitor/StatusApiResponse data with caching, replacing the removed /api/instatus route.
useNodeStatus hook, StatusBadge, and wiring
packages/core/hooks/use-node-status.ts, packages/ui/components/Static/status-badge.tsx, packages/ui/components/Layouts/Nodes/nodes-client.tsx, packages/ui/components/Static/footer.tsx, README.md, .env.example
Polls /api/status, renders a live StatusBadge in the footer, integrates live monitor state into Nodes cards/locations, and updates docs/env config accordingly.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Services grid styling and translations bump

Layer / File(s) Summary
Services grid card sizing adjustments
packages/ui/components/Layouts/Home/services.tsx
Adjusts grid column/width selection and card header/body spacing based on active service count.
Translations submodule bump
translations
Advances the tracked translations submodule commit reference.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
    participant NodesClient
    participant useNodeStatus
    participant StatusRoute as "/api/status"
    participant StatusLib as "status.ts"
    participant StatusAPI as "status.nodebyte.host"

    NodesClient->>useNodeStatus: mount, call hook
    useNodeStatus->>StatusRoute: fetch("/api/status")
    StatusRoute->>StatusLib: fetchStatusSnapshot()
    StatusLib->>StatusAPI: GET /api/v1/public/status
    StatusAPI-->>StatusLib: monitors + heartbeats
    StatusLib-->>StatusRoute: StatusSnapshot | null
    StatusRoute-->>useNodeStatus: StatusApiResponse (monitors, overallStatus)
    useNodeStatus-->>NodesClient: monitors, available, findMonitor
    NodesClient->>NodesClient: resolveNodeState per node/location
    loop every 60s
        useNodeStatus->>StatusRoute: re-fetch status
    end
Loading
sequenceDiagram
    participant BillingService as "billing-service.ts"
    participant Bytepay as "bytepay.ts"
    participant PaymenterAPI as "Paymenter admin API"
    participant SpecParser as "spec-parser.ts"
    participant Page as "Debug/Plan route"

    Page->>BillingService: getGamePlans/getDedicatedPlans/getVpsPlans(categorySlug)
    BillingService->>Bytepay: fetchAllBillingProducts() (cached)
    Bytepay->>PaymenterAPI: paginated GET products/categories
    PaymenterAPI-->>Bytepay: JSON:API pages
    Bytepay-->>BillingService: BillingProduct[]
    BillingService->>SpecParser: parseDescriptionSpecs/parseProductName
    SpecParser-->>BillingService: ParsedSpecs
    BillingService-->>Page: PlanSpec[] (with dropped-product warnings)
Loading

Possibly related PRs

  • NodeByteHosting/website#101: Both PRs touch the footer status UI, one relying on the old /api/instatus indicator that this PR replaces with StatusBadge.
  • NodeByteHosting/website#106: Both PRs modify packages/ui/components/Static/footer.tsx status-fetching logic, one swapping in StatusBadge and the other changing StatusIndicator behavior.
  • NodeByteHosting/website#120: Both PRs add the same billing stack (bytepay.ts, spec-parser.ts, billing-service.ts) and debug endpoint.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is too generic and does not describe the actual changes in the pull request. Rename it to reflect the main change, such as dedicated servers, KB routing, and status API integration.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

let cpuModel: string | undefined
let hardware: ParsedSpecs["hardware"]

const ryzenMatch = text.match(/AMD\s+Ryzen[™™]?\s+\d+(?:\s+(?:PRO\s+)?\d+\w*)?/i)
let hardware: ParsedSpecs["hardware"]

const ryzenMatch = text.match(/AMD\s+Ryzen[™™]?\s+\d+(?:\s+(?:PRO\s+)?\d+\w*)?/i)
const ampereMatch = text.match(/Ampere[®®]?\s+Altra[®®]?(?:\s+ARM64)?/i)
let hardware: ParsedSpecs["hardware"]

const ryzenMatch = text.match(/AMD\s+Ryzen[™™]?\s+\d+(?:\s+(?:PRO\s+)?\d+\w*)?/i)
const ampereMatch = text.match(/Ampere[®®]?\s+Altra[®®]?(?:\s+ARM64)?/i)
const ryzenMatch = text.match(/AMD\s+Ryzen[™™]?\s+\d+(?:\s+(?:PRO\s+)?\d+\w*)?/i)
const ampereMatch = text.match(/Ampere[®®]?\s+Altra[®®]?(?:\s+ARM64)?/i)
// Intel: Xeon, Core Ultra, Core i-series
const intelMatch = text.match(/Intel[®®]?\s+(?:Core[™™]?\s+Ultra\s+\d+(?:\s+\d+)?|Core[™™]?\s+i\d+[- ]\d+\w*|Xeon[®®]?(?:\s+\w+)*)/i)
const ryzenMatch = text.match(/AMD\s+Ryzen[™™]?\s+\d+(?:\s+(?:PRO\s+)?\d+\w*)?/i)
const ampereMatch = text.match(/Ampere[®®]?\s+Altra[®®]?(?:\s+ARM64)?/i)
// Intel: Xeon, Core Ultra, Core i-series
const intelMatch = text.match(/Intel[®®]?\s+(?:Core[™™]?\s+Ultra\s+\d+(?:\s+\d+)?|Core[™™]?\s+i\d+[- ]\d+\w*|Xeon[®®]?(?:\s+\w+)*)/i)
const ryzenMatch = text.match(/AMD\s+Ryzen[™™]?\s+\d+(?:\s+(?:PRO\s+)?\d+\w*)?/i)
const ampereMatch = text.match(/Ampere[®®]?\s+Altra[®®]?(?:\s+ARM64)?/i)
// Intel: Xeon, Core Ultra, Core i-series
const intelMatch = text.match(/Intel[®®]?\s+(?:Core[™™]?\s+Ultra\s+\d+(?:\s+\d+)?|Core[™™]?\s+i\d+[- ]\d+\w*|Xeon[®®]?(?:\s+\w+)*)/i)
const ryzenMatch = text.match(/AMD\s+Ryzen[™™]?\s+\d+(?:\s+(?:PRO\s+)?\d+\w*)?/i)
const ampereMatch = text.match(/Ampere[®®]?\s+Altra[®®]?(?:\s+ARM64)?/i)
// Intel: Xeon, Core Ultra, Core i-series
const intelMatch = text.match(/Intel[®®]?\s+(?:Core[™™]?\s+Ultra\s+\d+(?:\s+\d+)?|Core[™™]?\s+i\d+[- ]\d+\w*|Xeon[®®]?(?:\s+\w+)*)/i)
@CodeMeAPixel CodeMeAPixel merged commit fc1052c into production Jul 6, 2026
7 of 9 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (18)
packages/ui/components/Layouts/Home/services.tsx (1)

74-81: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

whitespace-nowrap on the service title risks overflow for long/localized names.

service.name comes from translations (t("servicesHome...") pattern elsewhere), and the title sits in a justify-between flex row next to a shrink-0 price badge. With whitespace-nowrap and no min-w-0/flex-shrink on the h3, a longer translated string could force the row wider than the card, causing the price badge to be pushed out or the title to visually clip (Card has overflow-hidden so it will clip rather than reflow).

💡 Proposed fix
-                  <h3 className="text-xl font-bold whitespace-nowrap">{service.name}</h3>
+                  <h3 className="text-xl font-bold truncate min-w-0">{service.name}</h3>
🤖 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/Home/services.tsx` around lines 74 - 81, The
service title in Home/services.tsx is forced onto one line in the header row,
which can overflow or clip longer translated names next to the price badge.
Update the title area around the service.name render to allow wrapping or
shrinking safely by removing the no-wrap behavior and adding appropriate flex
handling (for example, letting the h3 shrink within the justify-between row) so
long localized names don’t push the badge out of the card.
packages/kb/components/kb-sidebar.tsx (1)

46-46: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Open state won't re-sync on client-side navigation.

useState(() => isCategoryActive) runs its initializer only on mount. Since this is a client component driven by usePathname(), navigating between KB pages doesn't remount SidebarNode, so a category that becomes active after navigation stays collapsed even though its styling updates. Consider syncing open state when the node becomes active.

♻️ Possible approach
-  const [isOpen, setIsOpen] = useState(() => isCategoryActive)
+  const [isOpen, setIsOpen] = useState(() => isCategoryActive)
+
+  useEffect(() => {
+    if (isCategoryActive) setIsOpen(true)
+  }, [isCategoryActive])

Requires adding useEffect to the react import.

🤖 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/kb/components/kb-sidebar.tsx` at line 46, The open state in
SidebarNode is only initialized once from isCategoryActive, so it won’t update
when usePathname() changes during client-side navigation. Update SidebarNode to
sync isOpen whenever isCategoryActive becomes true, using a useEffect tied to
that value, and make sure useEffect is imported from react alongside useState.
packages/kb/lib/kb.ts (1)

284-298: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

getAllArticles re-walks the whole tree on every recursion.

getCategories(parentPath) already returns each category with its fully populated subcategories (recursively). Recursing again via getAllArticles(cat.path) re-invokes getCategories for each subtree, rebuilding trees that were just computed — redundant filesystem reads that grow with depth. You can traverse the already-materialized cat.subcategories instead, or flatten from a single getCategories() call.

🤖 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/kb/lib/kb.ts` around lines 284 - 298, `getAllArticles` is
redundantly re-reading the category tree on every recursive call by calling
`getCategories(cat.path)` again. Update `getAllArticles` to traverse the
already-populated `subcategories` returned by `getCategories` (or flatten from a
single top-level call) instead of recursing back into
`getAllArticles(cat.path)`, and keep using `getArticlesByCategory` for each
category path to collect articles.
packages/core/lib/bytepay.ts (1)

328-350: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate plan-resolution logic; fallback may pick a non-recurring plan.

getGbpPrice and getPricesMap both re-implement the same "recurring monthly → any recurring → plans[0]" fallback chain. Two issues:

  • Duplication: a future change to the fallback rule (e.g. preferring annual over one-time) needs to be kept in sync across both functions.
  • The final plans[0] fallback doesn't check type, so a product whose only plan is "one-time" or "free" will have that price surfaced as the "monthly" price for the plan card.

Consider extracting a shared resolveMonthlyPlan(product) helper used by both functions, and reconsidering whether non-recurring plans should ever be used as a price source.

♻️ Proposed refactor
+function resolveMonthlyPlan(product: BillingProduct): BillingPlan | undefined {
+  return (
+    product.plans.find((p) => p.type === "recurring" && p.billingPeriod === 1 && p.billingUnit === "month")
+    ?? product.plans.find((p) => p.type === "recurring")
+    ?? product.plans[0]
+  )
+}
+
 export function getGbpPrice(product: BillingProduct): number {
-  const plan =
-    product.plans.find((p) => p.type === "recurring" && p.billingPeriod === 1 && p.billingUnit === "month")
-    ?? product.plans.find((p) => p.type === "recurring")
-    ?? product.plans[0]
+  const plan = resolveMonthlyPlan(product)
   if (!plan) return 0
   return plan.prices.find((p) => p.currencyCode === "GBP")?.price ?? 0
 }

 export function getPricesMap(product: BillingProduct): Record<string, number> {
-  const plan =
-    product.plans.find((p) => p.type === "recurring" && p.billingPeriod === 1 && p.billingUnit === "month")
-    ?? product.plans.find((p) => p.type === "recurring")
-    ?? product.plans[0]
+  const plan = resolveMonthlyPlan(product)
   if (!plan) return {}
   ...
 }
🤖 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/core/lib/bytepay.ts` around lines 328 - 350, `getGbpPrice` and
`getPricesMap` duplicate the same plan selection logic, and both can incorrectly
fall back to a non-recurring plan via `plans[0]`. Extract the shared
recurring-plan resolution into a helper such as `resolveMonthlyPlan(product)`
and have both functions use it. While doing so, make sure the fallback chain
only returns a recurring plan (or otherwise explicitly handles the non-recurring
case) so one-time/free plans are not treated as monthly pricing.
packages/core/lib/spec-parser.ts (2)

151-167: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

series is returned unvalidated and later force-cast into a strict union.

parseProductName returns series as parts[1] ?? undefined with no check against known series codes. getVpsPlans in billing-service.ts then does series: series as VpsPlanSpec["series"], so any unexpected product-name segment (typo, new naming convention, etc.) is silently accepted as if it were a valid series, defeating the purpose of the union type.

Consider validating against the known series list here and returning undefined (with a warning) for unrecognized values, mirroring the warnDroppedProduct pattern already used elsewhere in this stack.

🤖 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/core/lib/spec-parser.ts` around lines 151 - 167, `parseProductName`
currently returns `series` from `parts[1]` without validation, which lets
invalid values flow into `getVpsPlans` where they are force-cast to
`VpsPlanSpec["series"]`. Update `parseProductName` to validate the parsed series
against the known series list, and return `undefined` for unrecognized values
while emitting a warning similar to `warnDroppedProduct`. Keep the change
localized to `parseProductName` so downstream callers like `getVpsPlans` no
longer need to rely on unsafe casts.

99-104: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Uplink silently defaults to a fabricated "1 Gbps" when not mentioned in the description.

When no Gbps figure is found, uplink is set to { amount: 1, unit: "Gbps" } rather than left undefined. This presents an invented spec as if it came from the billing panel; if the true port speed differs (or the field is legitimately not provided), customers see incorrect information.

🤖 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/core/lib/spec-parser.ts` around lines 99 - 104, The uplink parsing
logic in the spec-parser file is providing a fabricated default value of 1 Gbps
when no uplink speed is found in the text, rather than returning undefined.
Instead of defaulting uplinkMatch to the object { amount: 1, unit: "Gbps" } when
no match is found, change the ternary operator to set the uplink variable to
undefined when uplinkMatch is falsy, so that missing uplink information is
represented as undefined rather than as an invented specification.
app/api/billing-debug/route.ts (1)

24-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Debug price computation doesn't match production fallback logic.

gbpMonthly here only looks for an exact recurring/monthly plan, while getGbpPrice in packages/core/lib/bytepay.ts additionally falls back to "any recurring plan" then plans[0]. A product could show gbpMonthly: null in this debug endpoint while still rendering a (fallback) price on the live page, undermining the tool's purpose of surfacing pricing/parsing gaps.

🤖 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 `@app/api/billing-debug/route.ts` around lines 24 - 27, Align the `gbpMonthly`
debug computation with the fallback behavior used by `getGbpPrice` in
`packages/core/lib/bytepay.ts`: first try the exact recurring monthly GBP price,
then fall back to any recurring plan, and finally to `plans[0]` if needed.
Update the `route.ts` logic so this debug endpoint mirrors the live price
selection path and does not return null when production would still surface a
fallback price.
app/vps/page.tsx (1)

11-17: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider partial degradation instead of all-or-nothing fetch.

Promise.all means a failure fetching either shared-cpu or dedicated-cpu plans rejects the whole page render (no shared plans shown even if only dedicated-cpu failed, or vice versa). Promise.allSettled with per-category fallback to [] would let the page still render whichever category succeeded.

♻️ Proposed fix
 export default async function VpsPage() {
-  const [sharedPlans, dedicatedPlans] = await Promise.all([
-    getVpsPlans("shared-cpu"),
-    getVpsPlans("dedicated-cpu"),
-  ])
-  return <VpsHub plans={[...sharedPlans, ...dedicatedPlans]} />
+  const results = await Promise.allSettled([
+    getVpsPlans("shared-cpu"),
+    getVpsPlans("dedicated-cpu"),
+  ])
+  const plans = results.flatMap((r) => (r.status === "fulfilled" ? r.value : []))
+  return <VpsHub plans={plans} />
 }
🤖 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 `@app/vps/page.tsx` around lines 11 - 17, The VpsPage data fetch is
all-or-nothing because Promise.all in VpsPage rejects the entire render if
either getVpsPlans("shared-cpu") or getVpsPlans("dedicated-cpu") fails. Update
VpsPage to tolerate partial failures by using Promise.allSettled or equivalent
per-call handling, then fall back to an empty array for whichever category fails
while still passing the successful plans into VpsHub.
packages/ui/components/Layouts/VPS/vps-hub.tsx (1)

62-74: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Widened Series type loses compile-time key safety.

Series was narrowed to keyof typeof SERIES_META, now it's plain string, so any plan.series value can no longer be statically checked against SERIES_META. Runtime fallback (meta ? ... : s) prevents a crash but a mistyped series string will silently render its raw key without failing the build.

🤖 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/VPS/vps-hub.tsx` around lines 62 - 74,
`Series` was widened to plain `string`, which removes compile-time validation
against `SERIES_META` and allows mistyped series keys to slip through. Update
the `Series` type in `vps-hub.tsx` to be derived from `SERIES_META` (like the
existing `Lineup` pattern) and make sure any `plan.series` usage and related
helpers continue to reference the strongly typed `SERIES_META` keys so invalid
series values are caught at build time.
packages/core/lib/status.ts (3)

44-68: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider guarding this module against client-side import.

The docstring at Line 2 states STATUS_TOKEN is never sent to the browser, which holds only because this module is always invoked server-side. Adding the server-only package import would enforce that boundary at build time instead of relying on convention, preventing an accidental "use client" import (e.g. via a future refactor) from silently breaking this guarantee.

🔒 Proposed fix
+import "server-only"
+
 /**
  * Server-side only — STATUS_TOKEN is never sent to the browser.
  *
🤖 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/core/lib/status.ts` around lines 44 - 68, This module depends on
server-only behavior, so add a server-only import at the top of the status
module to enforce that it cannot be bundled into client code. Update the
status-related entry point around fetchStatusJson so the browser cannot
accidentally import it through a future refactor, preserving the STATUS_TOKEN
handling guarantee at build time.

77-97: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Unvalidated as casts on external API response.

fetchStatusSnapshot casts every field from the untrusted upstream JSON response (m.id as number, m.status as MonitorStatus, data.overall_status as MonitorStatus, etc.) without runtime validation. If status.nodebyte.host changes its schema or returns an unexpected value (e.g. a new status enum member, or null where a string is expected), these casts won't catch it — the bad data will propagate silently into StatusApiResponse and then into STATUS_DOT/STATUS_LABEL_KEY lookups in status-badge.tsx, which only handle known keys.

Consider adding a lightweight runtime validator (e.g. a Zod schema, or explicit typeof/enum checks) so malformed upstream payloads are caught in the existing catch block and degrade gracefully to null instead of shipping incorrect types downstream.

🤖 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/core/lib/status.ts` around lines 77 - 97, The `fetchStatusSnapshot`
parsing in `status.ts` is trusting unvalidated upstream JSON with `as` casts for
fields like `m.id`, `m.status`, and `data.overall_status`, so malformed or new
API values can leak into downstream consumers. Add runtime validation in
`fetchStatusSnapshot` (for example with a schema or explicit type/enum checks)
before building `monitors` and the returned object. Ensure unexpected or invalid
fields are rejected and handled by the existing `catch` path so the function
returns a safe fallback instead of propagating bad types.

112-123: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Spread-based Math.min/Math.max risks a stack overflow on large heartbeat arrays.

Math.min(...samples) / Math.max(...samples) push every element onto the call stack as an argument; large arrays (~100k+ elements in V8) throw RangeError: Maximum call stack size exceeded. Since heartbeats originates from an external, unvalidated API response (Line 86), an unexpectedly large payload would crash this computation — and since computeLatencyStats is called directly (unguarded) from app/api/status/route.ts, that would surface as a route failure rather than a graceful degradation.

A reduce-based implementation avoids the risk entirely at negligible cost.

🛡️ Proposed fix
   if (samples.length === 0) return null

-  const fast = Math.min(...samples)
-  const slow = Math.max(...samples)
+  const fast = samples.reduce((min, ms) => (ms < min ? ms : min), samples[0])
+  const slow = samples.reduce((max, ms) => (ms > max ? ms : max), samples[0])
   const avg = Math.round(samples.reduce((sum, ms) => sum + ms, 0) / samples.length)
   return { fast, avg, slow }
🤖 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/core/lib/status.ts` around lines 112 - 123, computeLatencyStats
currently uses spread with Math.min/Math.max over monitor.heartbeats, which can
overflow the call stack on large inputs. Update computeLatencyStats to derive
fast and slow with a reduce/loop-based approach over the filtered samples,
keeping the same return shape and using the existing samples logic.
packages/ui/components/Static/status-badge.tsx (1)

8-24: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Type these maps against MonitorStatus for exhaustiveness checking.

STATUS_DOT and STATUS_LABEL_KEY are declared as Record<string, string>. Typing them as Record<MonitorStatus, string> (imported from packages/core/lib/status) would make TypeScript flag any future addition/removal of a MonitorStatus member as a compile error here, instead of silently falling back to undefined styling/labels at runtime.

♻️ Proposed fix
+import type { MonitorStatus } from "`@/packages/core/lib/status`"

-const STATUS_DOT: Record<string, string> = {
+const STATUS_DOT: Record<MonitorStatus, string> = {
   up: "bg-green-400 animate-pulse",
   degraded: "bg-amber-400",
   maintenance: "bg-amber-400",
   paused: "bg-amber-400",
   down: "bg-red-400",
   unknown: "bg-muted-foreground",
 }

-const STATUS_LABEL_KEY: Record<string, string> = {
+const STATUS_LABEL_KEY: Record<MonitorStatus, string> = {
   up: "operational",
   degraded: "degraded",
   maintenance: "maintenance",
   paused: "maintenance",
   down: "down",
   unknown: "unavailable",
 }
🤖 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/Static/status-badge.tsx` around lines 8 - 24, Type the
STATUS_DOT and STATUS_LABEL_KEY maps in status-badge.tsx against MonitorStatus
instead of plain string keys so the compiler enforces exhaustiveness. Import
MonitorStatus from packages/core/lib/status, update both Record types to use it,
and keep the existing status-to-style/label mappings so any future MonitorStatus
change surfaces as a compile error in these constants.
packages/core/hooks/use-node-status.ts (1)

14-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Solid cleanup pattern; consider surfacing fetch errors.

The cancelled flag correctly prevents state updates after unmount, and the interval is properly cleared. One gap: .catch(() => {}) (Line 26) silently swallows fetch/parse failures with no way for consumers to distinguish "still loading," "fetched successfully," and "fetch failed" — all render as available: false. Exposing an error/loading field would let StatusBadge show a more accurate degraded state.

🤖 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/core/hooks/use-node-status.ts` around lines 14 - 35, The
useNodeStatus hook currently swallows request and parsing failures in the load
flow, so consumers cannot tell loading, success, and error states apart. Update
use-node-status’s useEffect/load logic to capture fetch failures instead of
ignoring them, and extend the hook’s returned state with an explicit error
and/or loading field alongside available, monitors, and overallStatus. Then
adjust StatusBadge to read that new state so it can display a degraded or error
status when /api/status fails.
.env.example (1)

9-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add trailing newline at end of file.

dotenv-linter flags a missing blank line at the end of the file after the new STATUS_TOKEN entry.

🧹 Proposed fix
 STATUS_API_URL="https://status.nodebyte.host"
-STATUS_TOKEN=""
+STATUS_TOKEN=""
+
🤖 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 @.env.example around lines 9 - 14, The .env.example file is missing the
required trailing blank line at the end of the file after the STATUS_TOKEN
entry, which dotenv-linter flags. Update the file so the final line is followed
by a newline/empty line, keeping the existing DEEPL_API_KEY, STATUS_API_URL, and
STATUS_TOKEN entries unchanged.

Source: Linters/SAST tools

packages/ui/components/Layouts/Nodes/nodes-client.tsx (3)

26-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type imported from an API route module into a UI component.

StatusApiMonitor is defined in app/api/status/route.ts and imported into this client component. It's import type, so it's erased at build time and safe functionally, but colocating the shared contract type in a route file couples the UI to route internals. Consider hoisting StatusApiMonitor/StatusApiResponse into packages/core/lib/status.ts (or a shared types module) for cleaner module boundaries.

🤖 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/Nodes/nodes-client.tsx` around lines 26 - 28,
The UI client component is importing the shared status contract type from an API
route module, which couples `nodes-client.tsx` to route internals. Move
`StatusApiMonitor` (and related `StatusApiResponse` if needed) into a shared
types module such as `packages/core/lib/status.ts`, then update
`nodes-client.tsx` and the status route to import from that shared location
instead of `app/api/status/route`.

121-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate style literals instead of reusing LIVE_STATE_STYLES.

The fallback branch re-declares the "Maintenance"/"Online" style objects verbatim instead of referencing LIVE_STATE_STYLES.maintenance / LIVE_STATE_STYLES.up, risking drift if one copy is updated without the other.

♻️ Suggested dedup
 function resolveNodeState(node: ExtendedNode, live: StatusApiMonitor | null) {
   if (live && live.status in LIVE_STATE_STYLES) {
     return LIVE_STATE_STYLES[live.status]
   }
-  return node.isMaintenanceMode
-    ? { label: "Maintenance", dot: "bg-amber-400", border: "hover:border-amber-500/30 hover:shadow-xl hover:shadow-amber-500/5", badge: "border-amber-500/30 text-amber-400 bg-amber-500/5" }
-    : { label: "Online", dot: "bg-green-400 animate-pulse", border: "hover:border-green-500/30 hover:shadow-xl hover:shadow-green-500/5", badge: "border-green-500/30 text-green-400 bg-green-500/5" }
+  return node.isMaintenanceMode ? LIVE_STATE_STYLES.maintenance : LIVE_STATE_STYLES.up
 }
🤖 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/Nodes/nodes-client.tsx` around lines 121 -
128, The fallback in resolveNodeState duplicates the Maintenance and Online
style objects instead of reusing the shared LIVE_STATE_STYLES entries. Update
the node.isMaintenanceMode branch to return LIVE_STATE_STYLES.maintenance and
the online branch to return LIVE_STATE_STYLES.up so resolveNodeState stays
consistent and avoids drift from duplicated literals.

156-156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Color derived via fragile string matching on class names/labels.

state.dot.includes("green"/"red") and state.label === "Online"/"Offline" re-derive a color from strings meant for CSS classes. It works today since all 4 states are covered, but it's brittle for future states and duplicates logic already encoded in LIVE_STATE_STYLES.

♻️ Suggested structured color field
-const LIVE_STATE_STYLES: Record<string, { label: string; dot: string; border: string; badge: string }> = {
-  up: { label: "Online", dot: "bg-green-400 animate-pulse", ... },
+const LIVE_STATE_STYLES: Record<string, { label: string; color: "green" | "amber" | "red"; dot: string; border: string; badge: string }> = {
+  up: { label: "Online", color: "green", dot: "bg-green-400 animate-pulse", ... },
   ...
 }

Then use state.color === "green" ? "bg-green-500" : ... instead of string matching on dot/label.

Also applies to: 161-164

🤖 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/Nodes/nodes-client.tsx` at line 156, The live
state indicator is re-deriving colors from display strings via
state.dot.includes(...) and state.label checks, which is brittle and duplicates
the intent already defined in LIVE_STATE_STYLES. Update the nodes-client.tsx
rendering logic to use a structured color field on the state object (for example
state.color) as the single source of truth for both the top bar and other
affected UI pieces, and replace the string matching in the live state markup
with direct color-based branching.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@app/api/status/route.ts`:
- Line 4: The fallback response in the status route is missing the same caching
behavior as the success path, so make the handler apply the same Cache-Control
header when it returns the 200 fallback. Update the status route handler logic
in the route’s response construction so both the normal and fallback responses
share the same cache window, using the existing status route response handling
as the reference point.

In `@app/games/hytale/page.tsx`:
- Around line 27-33: The Hytale plan mapping in the page render can crash when a
live billing slug is not present in HYTALE_PLAN_DISPLAY, because display may be
undefined before accessing display.name and display.description. Update the
plans transform in the page component to guard the lookup in the
getGamePlans("hytale") map, either by skipping plans with no matching display
entry or by providing a safe fallback object, and keep the logic centered around
the HYTALE_PLAN_DISPLAY and plans mapping in app/games/hytale/page.tsx.

In `@app/kb/`[...path]/page.tsx:
- Around line 111-115: The KB catch-all route in KBDynamicPage currently
forwards raw path segments from params into resolvePath and downstream
article/category lookup, so add validation before resolvePath to reject any
segments containing path traversal or separator characters such as .., /, or \.
If this page should only serve known routes, alternatively set dynamicParams =
false; otherwise ensure only safe, normalized segments can reach getArticle and
getArticlesByCategory.

In `@packages/core/constants/product-overrides.ts`:
- Around line 1-9: The documented SERIES_LOCATION mapping is missing, and
getVpsPlans currently hardcodes VPS location to undefined, so the datacenter
city is never populated. Add the SERIES_LOCATION export in product-overrides.ts
and use it in billing-service.ts within getVpsPlans to derive location from the
plan’s series code instead of assigning undefined. Make sure the new mapping is
referenced consistently by the existing series/location doc comments.

In `@packages/core/lib/spec-parser.ts`:
- Around line 106-120: The bandwidth parsing in spec-parser.ts only matches TB
values, so GB allowances are misclassified as null/unlimited. Update the
bandwidth extraction logic in the spec-parser function to recognize GB (and MB
if supported by ParsedSpecs["bandwidth"]) alongside TB, and ensure the unit
captured from the regex is preserved when constructing the
ParsedSpecs["bandwidth"] result. Keep the unlimitedBw check separate so only
true unmetered descriptions map to null.
- Around line 131-138: The Intel detection in spec-parser.ts is overriding
earlier AMD/ARM matches because the intelMatch branch in the parsing logic does
not check whether ryzenMatch or ampereMatch was already found. Update the CPU
detection flow in the relevant spec-parser logic so the Intel assignment only
runs when no AMD/ARM match has been established, matching the existing guards
used in the fallback branches for ryzenMatch and ampereMatch. Keep the
cpuModel/hardware precedence consistent within the same detection block.

In `@packages/core/products/billing-service.ts`:
- Around line 73-86: The JSDoc for getDedicatedPlans overstates the validation:
the implementation only filters out products missing parsed.ramGB, while cores
and storageGB may still be undefined and are returned as optional fields in
DedicatedPlanSpec. Update the comment to describe the actual ramGB-only
requirement, or, if cores/storage should be required, add matching checks in
getDedicatedPlans and keep warnDroppedProduct in sync with the rejected fields.

In `@packages/core/types/servers/dedicated.ts`:
- Line 20: DedicatedPlanSpec.hardware is missing the "arm" value even though
spec parsing and billing already pass it through at runtime. Update the
DedicatedPlanSpec type in the dedicated server type definition to include "arm"
alongside the existing hardware options, and make sure any related unions or
assertions that reference hardware stay aligned with the expanded set.

In `@packages/kb/components/kb-sidebar.tsx`:
- Around line 43-44: The active-state check in kb-sidebar’s category matching is
too broad because pathname.startsWith(categoryPrefix) can mark sibling
categories like game active when visiting games. Update the logic in
kb-sidebar.tsx around categoryPrefix/isCategoryActive to require a segment
boundary after the category path (while still allowing ancestor paths to remain
active), so only the exact category or its descendants match.

In `@packages/kb/lib/kb.ts`:
- Around line 168-179: The category sorting in kb.ts is incorrectly treating
order 0 as missing because the comparator uses a falsy fallback. Update the sort
logic in the return from the category-building function to use a numeric-safe
fallback that preserves legitimate zero values, and keep the change localized to
the categories.sort comparator so explicit order values from meta are respected.
- Around line 244-248: The articles sorting logic in kb.ts still treats order 0
as 999 in the comparator, so items with a valid zero order are placed last and
break prev/next navigation. Update the sorting in the article list builder that
returns articles.sort(...) to use a numeric comparison based on the actual order
values without falling back on truthiness, so order: 0 is preserved consistently
with the existing data.order ?? 999 assignment.

In `@packages/ui/components/Layouts/Nodes/nodes-client.tsx`:
- Around line 307-317: The aggregate “In Maintenance” stat is using a simple
onlineCount subtraction in NodesClient, which incorrectly buckets degraded/down
nodes as maintenance. Update the hero stat calculation to derive counts from the
same node-state logic used by each NodeCard, ideally via resolveNodeState, so
only true maintenance nodes are counted there and degraded/offline nodes remain
separate. Keep the aggregation consistent wherever maintenanceCount is computed
so the summary matches the per-node badges.

In `@packages/ui/components/Static/footer.tsx`:
- Around line 122-125: The footer Services entry for
`t("footer.services.systemStatus")` is hardcoded to the looking-glass URL
instead of the actual status page. Update the link in `Footer` to use the
existing `LINKS.status` constant, matching the `StatusBadge` behavior in this
file and avoiding the hardcoded `https://lg.nodebyte.host` value.

---

Nitpick comments:
In @.env.example:
- Around line 9-14: The .env.example file is missing the required trailing blank
line at the end of the file after the STATUS_TOKEN entry, which dotenv-linter
flags. Update the file so the final line is followed by a newline/empty line,
keeping the existing DEEPL_API_KEY, STATUS_API_URL, and STATUS_TOKEN entries
unchanged.

In `@app/api/billing-debug/route.ts`:
- Around line 24-27: Align the `gbpMonthly` debug computation with the fallback
behavior used by `getGbpPrice` in `packages/core/lib/bytepay.ts`: first try the
exact recurring monthly GBP price, then fall back to any recurring plan, and
finally to `plans[0]` if needed. Update the `route.ts` logic so this debug
endpoint mirrors the live price selection path and does not return null when
production would still surface a fallback price.

In `@app/vps/page.tsx`:
- Around line 11-17: The VpsPage data fetch is all-or-nothing because
Promise.all in VpsPage rejects the entire render if either
getVpsPlans("shared-cpu") or getVpsPlans("dedicated-cpu") fails. Update VpsPage
to tolerate partial failures by using Promise.allSettled or equivalent per-call
handling, then fall back to an empty array for whichever category fails while
still passing the successful plans into VpsHub.

In `@packages/core/hooks/use-node-status.ts`:
- Around line 14-35: The useNodeStatus hook currently swallows request and
parsing failures in the load flow, so consumers cannot tell loading, success,
and error states apart. Update use-node-status’s useEffect/load logic to capture
fetch failures instead of ignoring them, and extend the hook’s returned state
with an explicit error and/or loading field alongside available, monitors, and
overallStatus. Then adjust StatusBadge to read that new state so it can display
a degraded or error status when /api/status fails.

In `@packages/core/lib/bytepay.ts`:
- Around line 328-350: `getGbpPrice` and `getPricesMap` duplicate the same plan
selection logic, and both can incorrectly fall back to a non-recurring plan via
`plans[0]`. Extract the shared recurring-plan resolution into a helper such as
`resolveMonthlyPlan(product)` and have both functions use it. While doing so,
make sure the fallback chain only returns a recurring plan (or otherwise
explicitly handles the non-recurring case) so one-time/free plans are not
treated as monthly pricing.

In `@packages/core/lib/spec-parser.ts`:
- Around line 151-167: `parseProductName` currently returns `series` from
`parts[1]` without validation, which lets invalid values flow into `getVpsPlans`
where they are force-cast to `VpsPlanSpec["series"]`. Update `parseProductName`
to validate the parsed series against the known series list, and return
`undefined` for unrecognized values while emitting a warning similar to
`warnDroppedProduct`. Keep the change localized to `parseProductName` so
downstream callers like `getVpsPlans` no longer need to rely on unsafe casts.
- Around line 99-104: The uplink parsing logic in the spec-parser file is
providing a fabricated default value of 1 Gbps when no uplink speed is found in
the text, rather than returning undefined. Instead of defaulting uplinkMatch to
the object { amount: 1, unit: "Gbps" } when no match is found, change the
ternary operator to set the uplink variable to undefined when uplinkMatch is
falsy, so that missing uplink information is represented as undefined rather
than as an invented specification.

In `@packages/core/lib/status.ts`:
- Around line 44-68: This module depends on server-only behavior, so add a
server-only import at the top of the status module to enforce that it cannot be
bundled into client code. Update the status-related entry point around
fetchStatusJson so the browser cannot accidentally import it through a future
refactor, preserving the STATUS_TOKEN handling guarantee at build time.
- Around line 77-97: The `fetchStatusSnapshot` parsing in `status.ts` is
trusting unvalidated upstream JSON with `as` casts for fields like `m.id`,
`m.status`, and `data.overall_status`, so malformed or new API values can leak
into downstream consumers. Add runtime validation in `fetchStatusSnapshot` (for
example with a schema or explicit type/enum checks) before building `monitors`
and the returned object. Ensure unexpected or invalid fields are rejected and
handled by the existing `catch` path so the function returns a safe fallback
instead of propagating bad types.
- Around line 112-123: computeLatencyStats currently uses spread with
Math.min/Math.max over monitor.heartbeats, which can overflow the call stack on
large inputs. Update computeLatencyStats to derive fast and slow with a
reduce/loop-based approach over the filtered samples, keeping the same return
shape and using the existing samples logic.

In `@packages/kb/components/kb-sidebar.tsx`:
- Line 46: The open state in SidebarNode is only initialized once from
isCategoryActive, so it won’t update when usePathname() changes during
client-side navigation. Update SidebarNode to sync isOpen whenever
isCategoryActive becomes true, using a useEffect tied to that value, and make
sure useEffect is imported from react alongside useState.

In `@packages/kb/lib/kb.ts`:
- Around line 284-298: `getAllArticles` is redundantly re-reading the category
tree on every recursive call by calling `getCategories(cat.path)` again. Update
`getAllArticles` to traverse the already-populated `subcategories` returned by
`getCategories` (or flatten from a single top-level call) instead of recursing
back into `getAllArticles(cat.path)`, and keep using `getArticlesByCategory` for
each category path to collect articles.

In `@packages/ui/components/Layouts/Home/services.tsx`:
- Around line 74-81: The service title in Home/services.tsx is forced onto one
line in the header row, which can overflow or clip longer translated names next
to the price badge. Update the title area around the service.name render to
allow wrapping or shrinking safely by removing the no-wrap behavior and adding
appropriate flex handling (for example, letting the h3 shrink within the
justify-between row) so long localized names don’t push the badge out of the
card.

In `@packages/ui/components/Layouts/Nodes/nodes-client.tsx`:
- Around line 26-28: The UI client component is importing the shared status
contract type from an API route module, which couples `nodes-client.tsx` to
route internals. Move `StatusApiMonitor` (and related `StatusApiResponse` if
needed) into a shared types module such as `packages/core/lib/status.ts`, then
update `nodes-client.tsx` and the status route to import from that shared
location instead of `app/api/status/route`.
- Around line 121-128: The fallback in resolveNodeState duplicates the
Maintenance and Online style objects instead of reusing the shared
LIVE_STATE_STYLES entries. Update the node.isMaintenanceMode branch to return
LIVE_STATE_STYLES.maintenance and the online branch to return
LIVE_STATE_STYLES.up so resolveNodeState stays consistent and avoids drift from
duplicated literals.
- Line 156: The live state indicator is re-deriving colors from display strings
via state.dot.includes(...) and state.label checks, which is brittle and
duplicates the intent already defined in LIVE_STATE_STYLES. Update the
nodes-client.tsx rendering logic to use a structured color field on the state
object (for example state.color) as the single source of truth for both the top
bar and other affected UI pieces, and replace the string matching in the live
state markup with direct color-based branching.

In `@packages/ui/components/Layouts/VPS/vps-hub.tsx`:
- Around line 62-74: `Series` was widened to plain `string`, which removes
compile-time validation against `SERIES_META` and allows mistyped series keys to
slip through. Update the `Series` type in `vps-hub.tsx` to be derived from
`SERIES_META` (like the existing `Lineup` pattern) and make sure any
`plan.series` usage and related helpers continue to reference the strongly typed
`SERIES_META` keys so invalid series values are caught at build time.

In `@packages/ui/components/Static/status-badge.tsx`:
- Around line 8-24: Type the STATUS_DOT and STATUS_LABEL_KEY maps in
status-badge.tsx against MonitorStatus instead of plain string keys so the
compiler enforces exhaustiveness. Import MonitorStatus from
packages/core/lib/status, update both Record types to use it, and keep the
existing status-to-style/label mappings so any future MonitorStatus change
surfaces as a compile error in these constants.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e5a51e8c-e582-463b-9b0c-0e035092ded7

📥 Commits

Reviewing files that changed from the base of the PR and between 3034886 and 3fcc57e.

📒 Files selected for processing (63)
  • .env.example
  • CHANGELOG.md
  • README.md
  • app/api/billing-debug/route.ts
  • app/api/instatus/route.ts
  • app/api/status/route.ts
  • app/dedicated/page.tsx
  • app/games/hytale/page.tsx
  • app/games/minecraft/page.tsx
  • app/games/rust/page.tsx
  • app/kb/[...path]/page.tsx
  • app/kb/[category]/[article]/page.tsx
  • app/kb/[category]/page.tsx
  • app/kb/page.tsx
  • app/vps/page.tsx
  • packages/core/constants/game/hytale.ts
  • packages/core/constants/links.ts
  • packages/core/constants/product-overrides.ts
  • packages/core/constants/services.ts
  • packages/core/constants/status-mapping.ts
  • packages/core/hooks/use-node-status.ts
  • packages/core/lib/bytepay.ts
  • packages/core/lib/spec-parser.ts
  • packages/core/lib/status.ts
  • packages/core/products/billing-service.ts
  • packages/core/types/servers/dedicated.ts
  • packages/core/types/servers/game.ts
  • packages/core/types/servers/vps.ts
  • packages/kb/components/kb-article-card.tsx
  • packages/kb/components/kb-article.tsx
  • packages/kb/components/kb-category-card.tsx
  • packages/kb/components/kb-search.tsx
  • packages/kb/components/kb-sidebar.tsx
  • packages/kb/content/billing/_meta.json
  • packages/kb/content/games/hytale/operating-yourself.md
  • packages/kb/content/games/hytale/setting-motd.md
  • packages/kb/content/games/hytale/setting-server-name.md
  • packages/kb/content/games/hytale/setting-server-password.md
  • packages/kb/content/games/hytale/setting-up-hytale.md
  • packages/kb/content/games/rust/_meta.json
  • packages/kb/content/games/rust/add-additional-ports.md
  • packages/kb/content/games/rust/connect-to-server.md
  • packages/kb/content/games/rust/enabling-rust+.md
  • packages/kb/content/games/rust/getting-started.md
  • packages/kb/content/games/rust/oxide-installation.md
  • packages/kb/lib/kb.ts
  • packages/ui/components/Layouts/About/about-page.tsx
  • packages/ui/components/Layouts/Contact/contact.tsx
  • packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx
  • packages/ui/components/Layouts/Error/error-page.tsx
  • packages/ui/components/Layouts/Error/not-found-page.tsx
  • packages/ui/components/Layouts/Games/game-pricing.tsx
  • packages/ui/components/Layouts/Home/hero.tsx
  • packages/ui/components/Layouts/Home/services.tsx
  • packages/ui/components/Layouts/Nodes/nodes-client.tsx
  • packages/ui/components/Layouts/VPS/vps-hero.tsx
  • packages/ui/components/Layouts/VPS/vps-hub.tsx
  • packages/ui/components/Static/footer.tsx
  • packages/ui/components/Static/navigation.tsx
  • packages/ui/components/Static/status-badge.tsx
  • packages/ui/components/theme-toggle.tsx
  • packages/ui/components/ui/price.tsx
  • translations
💤 Files with no reviewable changes (3)
  • app/kb/[category]/page.tsx
  • app/kb/[category]/[article]/page.tsx
  • app/api/instatus/route.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (13)
app/api/status/route.ts (1)

4-4: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Next.js 16 route handler revalidate export without dynamic force-static caching behavior

💡 Result:

In Next.js 16, exporting revalidate from a Route Handler dictates the cache duration for that route segment [1][2]. Setting export const revalidate = <number> tells Next.js to cache the response and revalidate it after the specified number of seconds [2]. Crucially, you can use revalidate without explicitly setting export const dynamic = 'force-static'. Under this configuration, the Route Handler will behave as follows: 1. Static Caching: By setting a positive revalidate value, you are opting the Route Handler into static rendering/caching behavior [2]. Even without force-static, the presence of a revalidation interval typically informs Next.js that the route is eligible for static generation if no dynamic APIs (like cookies() or headers()) are accessed [3][2]. 2. Interaction with dynamic: If you do not set dynamic, Next.js defaults to 'auto' [2]. When revalidate is present, it effectively influences the heuristic used by the compiler to determine if the route should be static [2]. 3. Stale-While-Revalidate (SWR): When the cache expires after the revalidate duration, the next request will serve the stale cached content while the Route Handler re-runs in the background to refresh the cache [4][5]. Important Note for Next.js 16: If your Route Handler uses functions like revalidateTag(), note that in Next.js 16, this function now requires a second argument (a cacheLife profile, e.g., 'max') to enable SWR behavior [4][6]. The previous behavior of revalidating immediately (or triggering a cache miss) has been deprecated and now often requires explicit configuration (such as { expire: 0 } for immediate expiration) [5]. Summary of Behavior: - With revalidate set: The route is cached. It will be served stale and updated in the background based on the specified interval [4][2]. - Without force-static: The route remains "static" as long as it doesn't encounter dynamic functions at runtime [3][2]. If it does use dynamic functions, the revalidate export may be ignored or conflict, leading to dynamic execution [2][7]. It is best practice to keep Route Handlers clean if you intend for them to be cached statically [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file list ==\n'
git ls-files app/api/status/route.ts app/api/status -z | tr '\0' '\n'

printf '\n== route.ts lines ==\n'
cat -n app/api/status/route.ts

printf '\n== surrounding references to fetchStatusSnapshot ==\n'
rg -n "fetchStatusSnapshot|Cache-Control|revalidate|dynamic =" app/api/status -S

Repository: NodeByteHosting/website

Length of output: 2458


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== route.ts outline ==\n'
ast-grep outline app/api/status/route.ts --view expanded

printf '\n== next config and route-handler docs references in repo ==\n'
rg -n "dynamic = 'force-static'|dynamic = \"force-static\"|revalidate =|Route Handler|route handler|cache" -S . --glob '!**/node_modules/**' --glob '!**/.next/**' | head -n 200

Repository: NodeByteHosting/website

Length of output: 5936


🌐 Web query:

Next.js route handler revalidate cached response docs dynamic auto force-static official

💡 Result:

In Next.js, Route Handlers are not cached by default [1]. To control caching and revalidation behavior, you can use segment configuration options and on-demand revalidation functions [2]. Caching and Revalidation Options 1. Segment Configuration: You can control the caching behavior of a Route Handler by exporting specific configuration constants from your route file (e.g., route.ts) [2][3]. - export const dynamic = 'force-static': This forces the route to be prerendered and cached, effectively turning it into a static route [2][4]. - export const revalidate = n: Sets the default revalidation frequency for the route to n seconds [2]. - export const dynamic = 'auto' (default): Automatically determines whether the route should be static or dynamic based on its use of Request-time APIs like cookies or headers [2][5]. 2. On-demand Revalidation: You can invalidate cached data programmatically using functions in a Server Action or Route Handler [2]. - revalidatePath(path): Invalidates all cached data associated with a specific path [6][2]. - revalidateTag(tag): Invalidates cached data associated with a specific cache tag [2][7]. Static Generation with generateStaticParams To statically generate routes at build time (often used with force-static or dynamicParams config), you can use generateStaticParams [8][4]. This allows you to define the set of parameters for which the route should be prerendered [4]. When using dynamic segments, you must return an array (even if empty) from generateStaticParams to allow for ISR (Incremental Static Regeneration) at runtime [4]. Precedence Segment configuration options generally take precedence over individual fetch options [5]. For example, if a segment is configured with force-dynamic, all fetch requests within that segment will behave as dynamic regardless of their individual cache settings [5]. It is recommended to carefully review the Next.js precedence documentation when mixing segment-level and fetch-level configurations [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the file if present and route handler docs references in the repo.
cat -n app/api/status/route.ts 2>/dev/null || true
printf '\n--\n'
rg -n "fetchStatusSnapshot|Cache-Control|export const revalidate|export const dynamic" app/api/status -S 2>/dev/null || true

Repository: NodeByteHosting/website

Length of output: 2346


🌐 Web query:

Next.js route handler segment config revalidate official docs route.ts caching behavior

💡 Result:

In Next.js, Route Handlers (route.ts) utilize Route Segment Config to control caching and revalidation behavior [1][2]. Caching Behavior: Route Handlers are not cached by default [3]. However, you can opt into caching specifically for GET methods [3]. Other HTTP methods (POST, PUT, DELETE, etc.) are not cached [3]. A known limitation is that if a route.ts file contains both a GET method and other HTTP methods, the file may not be cached, as the caching mechanism is currently method-invariant [4]. Segment Config Options: You can configure these behaviors by exporting specific variables from your route.ts file [1][5]: 1. revalidate: This option allows you to set the cache lifetime in seconds (e.g., export const revalidate = 60) [6]. 2. dynamic: You can use export const dynamic = 'force-static' to force a route to be statically rendered (cached) or 'force-dynamic' to ensure it is always executed dynamically [1][7][8]. Revalidation: Beyond static revalidation (using the revalidate config), you can programmatically revalidate cached data using functions like revalidatePath [9]. Note that in newer versions of Next.js, specific segment config options like revalidate and dynamic may be subject to changes as the framework transitions toward new caching models (e.g., the Cache Components model) [7]. Always check your specific Next.js version documentation for the most accurate implementation details [1][7].

Citations:


Cache the fallback response too app/api/status/route.ts:26-30 returns 200 without the Cache-Control header used on the success path, so outage responses can bypass the same cache window and keep hammering the origin.

🤖 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 `@app/api/status/route.ts` at line 4, The fallback response in the status route
is missing the same caching behavior as the success path, so make the handler
apply the same Cache-Control header when it returns the 200 fallback. Update the
status route handler logic in the route’s response construction so both the
normal and fallback responses share the same cache window, using the existing
status route response handling as the reference point.
app/games/hytale/page.tsx (1)

27-33: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== app/games/hytale/page.tsx ==\n'
if [ -f app/games/hytale/page.tsx ]; then
  cat -n app/games/hytale/page.tsx | sed -n '1,220p'
else
  echo "Missing app/games/hytale/page.tsx"
fi

printf '\n== Search for HYTALE_PLAN_DISPLAY and getGamePlans ==\n'
rg -n "HYTALE_PLAN_DISPLAY|getGamePlans\\(" app lib src . --glob '!node_modules' --glob '!dist' --glob '!build' || true

printf '\n== Files containing GamePlanSpec or plan.id ==\n'
rg -n "GamePlanSpec|plan\\.id|product\\.slug|GamePlan" app lib src . --glob '!node_modules' --glob '!dist' --glob '!build' || true

Repository: NodeByteHosting/website

Length of output: 12537


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== packages/core/constants/game/hytale.ts ==\n'
cat -n packages/core/constants/game/hytale.ts | sed -n '1,220p'

printf '\n== packages/core/products/billing-service.ts ==\n'
cat -n packages/core/products/billing-service.ts | sed -n '1,220p'

printf '\n== packages/core/types/servers/game.ts ==\n'
cat -n packages/core/types/servers/game.ts | sed -n '1,220p'

Repository: NodeByteHosting/website

Length of output: 15579


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Search for Hytale slug generation and product seeding ==\n'
rg -n '"hytale-(starter|standard|performance)"|fromGamePlan|planId: plan\.id|slug:|product\.slug' packages app --glob '!node_modules' --glob '!dist' --glob '!build' || true

printf '\n== packages/core/products/service.ts ==\n'
cat -n packages/core/products/service.ts | sed -n '1,220p'

printf '\n== app/api/billing-debug/route.ts ==\n'
cat -n app/api/billing-debug/route.ts | sed -n '1,220p'

Repository: NodeByteHosting/website

Length of output: 11100


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Search for billing sync / Paymenter source-of-truth notes ==\n'
rg -n "Paymenter|billing panel|source of truth|sync.*billing|seed.*billing|mirror.*billing|live billing|billing products" packages app README.md CHANGELOG.md --glob '!node_modules' --glob '!dist' --glob '!build' || true

printf '\n== packages/core/lib/bytepay.ts ==\n'
cat -n packages/core/lib/bytepay.ts | sed -n '1,320p'

Repository: NodeByteHosting/website

Length of output: 14158


Guard the Hytale plan lookup at app/games/hytale/page.tsx:27-33
HYTALE_PLAN_DISPLAY[plan.id as keyof typeof HYTALE_PLAN_DISPLAY] can be undefined for a live billing slug that doesn't match the hardcoded keys, and display.name will throw during render. Skip unknown plans or provide a fallback display entry.

🤖 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 `@app/games/hytale/page.tsx` around lines 27 - 33, The Hytale plan mapping in
the page render can crash when a live billing slug is not present in
HYTALE_PLAN_DISPLAY, because display may be undefined before accessing
display.name and display.description. Update the plans transform in the page
component to guard the lookup in the getGamePlans("hytale") map, either by
skipping plans with no matching display entry or by providing a safe fallback
object, and keep the logic centered around the HYTALE_PLAN_DISPLAY and plans
mapping in app/games/hytale/page.tsx.
app/kb/[...path]/page.tsx (1)

111-115: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP 'export\s+const\s+dynamicParams' app/kb

Repository: NodeByteHosting/website

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## app/kb/[...path]/page.tsx (outline)\n'
ast-grep outline app/kb/'[...path]'/page.tsx --view expanded || true

printf '\n## app/kb/[...path]/page.tsx (selected lines)\n'
cat -n app/kb/'[...path]'/page.tsx | sed -n '1,220p'

printf '\n## locate kb.ts and outline\n'
fd -a 'kb.ts' .
ast-grep outline app/kb/kb.ts --view expanded || true

printf '\n## app/kb/kb.ts (selected lines)\n'
cat -n app/kb/kb.ts | sed -n '1,320p'

printf '\n## search for path sanitization / dynamicParams under app/kb\n'
rg -n "dynamicParams|resolvePath|getArticle|getArticlesByCategory|notFound\\(|\\.join\\(KB_CONTENT_PATH" app/kb

Repository: NodeByteHosting/website

Length of output: 9892


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## packages/kb/lib/kb.ts (outline)\n'
ast-grep outline packages/kb/lib/kb.ts --view expanded || true

printf '\n## packages/kb/lib/kb.ts (selected lines 1-360)\n'
cat -n packages/kb/lib/kb.ts | sed -n '1,360p'

printf '\n## search for path validation in packages/kb/lib/kb.ts\n'
rg -n "resolvePath|getCategoryAtPath|getArticlesByCategory|getArticle|path\\.join|normalize|sanitize|\\.\\. |\\.\\.\\/|\\0|sep" packages/kb/lib/kb.ts

Repository: NodeByteHosting/website

Length of output: 16646


Validate KB path segments in app/kb/[...path]/page.tsx
The catch-all params flow straight into resolvePath / getArticle / getArticlesByCategory, which path.join(...) them against KB_CONTENT_PATH. Reject ../path-separator segments here, or set dynamicParams = false if only prebuilt paths should resolve.

🤖 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 `@app/kb/`[...path]/page.tsx around lines 111 - 115, The KB catch-all route in
KBDynamicPage currently forwards raw path segments from params into resolvePath
and downstream article/category lookup, so add validation before resolvePath to
reject any segments containing path traversal or separator characters such as
.., /, or \. If this page should only serve known routes, alternatively set
dynamicParams = false; otherwise ensure only safe, normalized segments can reach
getArticle and getArticlesByCategory.
packages/core/constants/product-overrides.ts (1)

1-9: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Documented SERIES_LOCATION map is missing; VPS location is hardcoded to undefined.

The header comment describes a SERIES_LOCATION export mapping series codes to datacenter cities, and getVpsPlans in packages/core/products/billing-service.ts documents the same ("Series→location mapping ... come from product-overrides.ts"). Neither this file nor billing-service.ts actually implements it — billing-service.ts line 140 sets location: undefined unconditionally, so every VPS plan silently loses its datacenter location.

Either this constant was dropped during refactor, or the doc comments are stale. If location display is expected on the VPS pages, this needs to be added.

🤖 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/core/constants/product-overrides.ts` around lines 1 - 9, The
documented SERIES_LOCATION mapping is missing, and getVpsPlans currently
hardcodes VPS location to undefined, so the datacenter city is never populated.
Add the SERIES_LOCATION export in product-overrides.ts and use it in
billing-service.ts within getVpsPlans to derive location from the plan’s series
code instead of assigning undefined. Make sure the new mapping is referenced
consistently by the existing series/location doc comments.
packages/core/lib/spec-parser.ts (2)

106-120: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Bandwidth parsing only recognizes TB, silently turning GB allowances into "unlimited".

ParsedSpecs["bandwidth"] supports "MB" | "GB" | "TB", but every regex here (bwTbMatch) only matches \d+\s+TB. If a plan's description states its allowance in GB (e.g. "500 GB outbound traffic"), none of the patterns match, bwTbMatch is null, and the code falls through to bandwidth: null — which downstream treats as unmetered/unlimited. That's a materially wrong claim to surface on a billing page for a metered plan.

🔧 Proposed fix
   const bwTbMatch =
     text.match(/(\d+)\s+TB\s+(?:included\s+)?outbound/i) ||
     text.match(/(\d+)\s+TB\s+of\s+(?:poolable\s+)?outbound/i) ||
     text.match(/(\d+)\s+TB\s+Outbound/i) ||
     text.match(/(\d+)\s+TB\b[^.]*?(?:traffic|bandwidth)/i)
+  const bwGbMatch = !bwTbMatch && (
+    text.match(/(\d+)\s+GB\s+(?:included\s+)?outbound/i) ||
+    text.match(/(\d+)\s+GB\s+of\s+(?:poolable\s+)?outbound/i) ||
+    text.match(/(\d+)\s+GB\s+Outbound/i)
+  )
   const bandwidth: ParsedSpecs["bandwidth"] = unlimitedBw
     ? null
     : bwTbMatch
       ? { amount: parseInt(bwTbMatch[1]), unit: "TB" }
+      : bwGbMatch
+        ? { amount: parseInt(bwGbMatch[1]), unit: "GB" }
       : null
📝 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.

  // ── Bandwidth ──────────────────────────────────────────────────────────────
  // "1 TB included outbound", "20 TB Outbound Traffic Pool", "4 TB of poolable outbound"
  // "Unlimited Outbound Bandwidth" → null (unmetered)
  // Avoids false positives from storage descriptions ("4 x 16 TB Enterprise SATA HDDs")
  const unlimitedBw = /unlimited\s+(?:\w+\s+)?bandwidth/i.test(text)
  const bwTbMatch =
    text.match(/(\d+)\s+TB\s+(?:included\s+)?outbound/i) ||
    text.match(/(\d+)\s+TB\s+of\s+(?:poolable\s+)?outbound/i) ||
    text.match(/(\d+)\s+TB\s+Outbound/i) ||
    text.match(/(\d+)\s+TB\b[^.]*?(?:traffic|bandwidth)/i)
  const bwGbMatch = !bwTbMatch && (
    text.match(/(\d+)\s+GB\s+(?:included\s+)?outbound/i) ||
    text.match(/(\d+)\s+GB\s+of\s+(?:poolable\s+)?outbound/i) ||
    text.match(/(\d+)\s+GB\s+Outbound/i)
  )
  const bandwidth: ParsedSpecs["bandwidth"] = unlimitedBw
    ? null
    : bwTbMatch
      ? { amount: parseInt(bwTbMatch[1]), unit: "TB" }
      : bwGbMatch
        ? { amount: parseInt(bwGbMatch[1]), unit: "GB" }
      : null
🤖 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/core/lib/spec-parser.ts` around lines 106 - 120, The bandwidth
parsing in spec-parser.ts only matches TB values, so GB allowances are
misclassified as null/unlimited. Update the bandwidth extraction logic in the
spec-parser function to recognize GB (and MB if supported by
ParsedSpecs["bandwidth"]) alongside TB, and ensure the unit captured from the
regex is preserved when constructing the ParsedSpecs["bandwidth"] result. Keep
the unlimitedBw check separate so only true unmetered descriptions map to null.

131-138: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Intel branch unconditionally overwrites an already-detected AMD/ARM match.

Line 137's if (intelMatch) has no guard against ryzenMatch/ampereMatch, unlike its own else if fallback on line 138 which does check !ryzenMatch && !ampereMatch. If a description matches both the Ryzen/Ampere pattern and the (fairly broad) Intel pattern — e.g. marketing copy comparing against Intel — the final hardware/cpuModel silently becomes "intel", misclassifying the CPU brand shown to customers.

🐛 Proposed fix
-  if (intelMatch) { cpuModel = intelMatch[0].trim(); hardware = "intel" }
+  if (intelMatch && !ryzenMatch && !ampereMatch) { cpuModel = intelMatch[0].trim(); hardware = "intel" }
   else if (/\bintel\b/i.test(text) && !ryzenMatch && !ampereMatch) { hardware = "intel" }
📝 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.

  if (ryzenMatch) { cpuModel = ryzenMatch[0].trim(); hardware = "amd" }
  else if (/\bamd\b/i.test(text)) { hardware = "amd" }

  if (ampereMatch) { cpuModel = "Ampere® Altra® ARM64"; hardware = "arm" }
  else if (/\barm64?\b/i.test(text) && !ryzenMatch) { hardware = "arm" }

  if (intelMatch && !ryzenMatch && !ampereMatch) { cpuModel = intelMatch[0].trim(); hardware = "intel" }
  else if (/\bintel\b/i.test(text) && !ryzenMatch && !ampereMatch) { hardware = "intel" }
🤖 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/core/lib/spec-parser.ts` around lines 131 - 138, The Intel detection
in spec-parser.ts is overriding earlier AMD/ARM matches because the intelMatch
branch in the parsing logic does not check whether ryzenMatch or ampereMatch was
already found. Update the CPU detection flow in the relevant spec-parser logic
so the Intel assignment only runs when no AMD/ARM match has been established,
matching the existing guards used in the fallback branches for ryzenMatch and
ampereMatch. Keep the cpuModel/hardware precedence consistent within the same
detection block.
packages/core/products/billing-service.ts (1)

73-86: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Doc comment overstates the validation actually performed.

The function comment claims plans "missing cores/ram/storage" are skipped, but the guard below only checks !parsed.ramGB. cores and storageGB can be undefined and the plan is still returned (consistent with them being optional on DedicatedPlanSpec). Please update the comment to match the actual (ramGB-only) requirement, or tighten the check if cores/storage really should be mandatory.

🤖 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/core/products/billing-service.ts` around lines 73 - 86, The JSDoc
for getDedicatedPlans overstates the validation: the implementation only filters
out products missing parsed.ramGB, while cores and storageGB may still be
undefined and are returned as optional fields in DedicatedPlanSpec. Update the
comment to describe the actual ramGB-only requirement, or, if cores/storage
should be required, add matching checks in getDedicatedPlans and keep
warnDroppedProduct in sync with the rejected fields.
packages/core/types/servers/dedicated.ts (1)

20-20: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files 'packages/core/types/servers/dedicated.ts' \
  'packages/core/lib/spec-parser.ts' \
  'packages/core/products/billing-service.ts' \
  'packages/core/**/*' | sed -n '1,200p'

printf '\n== dedicated type ==\n'
sed -n '1,120p' packages/core/types/servers/dedicated.ts

printf '\n== spec parser slice ==\n'
sed -n '1,220p' packages/core/lib/spec-parser.ts

printf '\n== billing service slice ==\n'
sed -n '1,180p' packages/core/products/billing-service.ts

printf '\n== searches ==\n'
rg -n 'DedicatedPlanSpec\\[\"hardware\"\\]|hardware: parsed\\.hardware|hardware\\?: "amd" \\| "intel" \\| "arm"|brand logo|logo.*hardware|switch.*hardware' packages/core -S

Repository: NodeByteHosting/website

Length of output: 15810


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
print("noop")
PY

Repository: NodeByteHosting/website

Length of output: 166


Add "arm" to DedicatedPlanSpec.hardware

packages/core/lib/spec-parser.ts can emit hardware: "arm", and packages/core/products/billing-service.ts forwards that value into DedicatedPlanSpec via a type assertion. As written, dedicated plans can carry an ARM hardware value at runtime while the type still only allows "amd" | "intel".

🔧 Proposed fix
-  hardware?: "amd" | "intel"
+  hardware?: "amd" | "intel" | "arm"
📝 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.

  hardware?: "amd" | "intel" | "arm"
🤖 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/core/types/servers/dedicated.ts` at line 20,
DedicatedPlanSpec.hardware is missing the "arm" value even though spec parsing
and billing already pass it through at runtime. Update the DedicatedPlanSpec
type in the dedicated server type definition to include "arm" alongside the
existing hardware options, and make sure any related unions or assertions that
reference hardware stay aligned with the expanded set.
packages/kb/components/kb-sidebar.tsx (1)

43-44: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prefix-based active check can highlight sibling categories.

pathname.startsWith(categoryPrefix) treats any category whose path is a string prefix of another as active. For sibling categories like game and games, visiting /kb/games/... also marks game active. Anchor the match to a segment boundary while preserving ancestor highlighting.

🐛 Proposed fix
   const categoryPrefix = `/kb/${item.path}`
-  const isCategoryActive = pathname.startsWith(categoryPrefix)
+  const isCategoryActive =
+    pathname === categoryPrefix || pathname.startsWith(`${categoryPrefix}/`)
📝 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.

  const categoryPrefix = `/kb/${item.path}`
  const isCategoryActive =
    pathname === categoryPrefix || pathname.startsWith(`${categoryPrefix}/`)
🤖 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/kb/components/kb-sidebar.tsx` around lines 43 - 44, The active-state
check in kb-sidebar’s category matching is too broad because
pathname.startsWith(categoryPrefix) can mark sibling categories like game active
when visiting games. Update the logic in kb-sidebar.tsx around
categoryPrefix/isCategoryActive to require a segment boundary after the category
path (while still allowing ancestor paths to remain active), so only the exact
category or its descendants match.
packages/kb/lib/kb.ts (2)

168-179: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

order || 999 drops legitimate order: 0, mis-sorting the category.

meta.order is set via fileMeta.order ?? 999, so an explicit order: 0 (e.g. content/billing/_meta.json) is preserved as 0. But the comparator uses a.order || 999, which coerces 0 → 999, pushing that category to the bottom instead of the top. Since order is always a number here, drop the || fallback.

🐛 Proposed fix
-  return categories.sort((a, b) => (a.order || 999) - (b.order || 999))
+  return categories.sort((a, b) => a.order - b.order)
📝 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.

    categories.push({
      slug: dir.name,
      path: catPath,
      parentPath,
      ...meta,
      articleCount: directArticleCount,
      totalCount,
      subcategories,
    })
  }

  return categories.sort((a, b) => a.order - b.order)
🤖 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/kb/lib/kb.ts` around lines 168 - 179, The category sorting in kb.ts
is incorrectly treating order 0 as missing because the comparator uses a falsy
fallback. Update the sort logic in the return from the category-building
function to use a numeric-safe fallback that preserves legitimate zero values,
and keep the change localized to the categories.sort comparator so explicit
order values from meta are respected.

244-248: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Same order: 0 sorting bug for articles.

order: data.order ?? 999 preserves 0, but the comparator (a.order || 999) coerces 0 → 999, so an article with order: 0 sorts last. This also skews prev/next navigation in the catch-all page, which relies on this ordering. Use a plain numeric comparator.

🐛 Proposed fix
-  return articles.sort((a, b) => (a.order || 999) - (b.order || 999))
+  return articles.sort((a, b) => a.order - b.order)
📝 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.

      order: data.order ?? 999,
    })
  }

  return articles.sort((a, b) => a.order - b.order)
🤖 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/kb/lib/kb.ts` around lines 244 - 248, The articles sorting logic in
kb.ts still treats order 0 as 999 in the comparator, so items with a valid zero
order are placed last and break prev/next navigation. Update the sorting in the
article list builder that returns articles.sort(...) to use a numeric comparison
based on the actual order values without falling back on truthiness, so order: 0
is preserved consistently with the existing data.order ?? 999 assignment.
packages/ui/components/Layouts/Nodes/nodes-client.tsx (1)

307-317: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Aggregate "In Maintenance" stat misrepresents degraded/down nodes.

maintenanceCount is derived as nodes.length - onlineCount, so any node whose live status is "degraded" or "down" (not "up") gets folded into "In Maintenance" in the hero stats, even though its own NodeCard badge correctly shows "Degraded"/"Offline". This creates a user-visible inconsistency between the per-node state and the aggregate count.

🔧 Suggested fix using resolveNodeState for consistent bucketing
-  const nodeLiveStates = nodes.map((node) => {
-    const monitorName = NODE_MONITOR_MAP[node.name]
-    return monitorName ? findMonitor(monitorName) : null
-  })
-  const onlineCount = nodeLiveStates.filter(
-    (live, i) => (live ? live.status === "up" : !nodes[i].isMaintenanceMode),
-  ).length
-  const maintenanceCount = nodes.length - onlineCount
+  const nodeLiveStates = nodes.map((node) => {
+    const monitorName = NODE_MONITOR_MAP[node.name]
+    return monitorName ? findMonitor(monitorName) : null
+  })
+  const nodeStates = nodes.map((node, i) => resolveNodeState(node, nodeLiveStates[i]))
+  const onlineCount = nodeStates.filter((s) => s.label === "Online").length
+  const maintenanceCount = nodeStates.filter((s) => s.label === "Maintenance").length

Also applies to: 350-354

🤖 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/Nodes/nodes-client.tsx` around lines 307 -
317, The aggregate “In Maintenance” stat is using a simple onlineCount
subtraction in NodesClient, which incorrectly buckets degraded/down nodes as
maintenance. Update the hero stat calculation to derive counts from the same
node-state logic used by each NodeCard, ideally via resolveNodeState, so only
true maintenance nodes are counted there and degraded/offline nodes remain
separate. Keep the aggregation consistent wherever maintenanceCount is computed
so the summary matches the per-node badges.
packages/ui/components/Static/footer.tsx (1)

122-125: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

"System Status" footer link likely points to the wrong URL.

The Services list labels this link "System Status" but hardcodes https://lg.nodebyte.host (the network looking-glass, matching LINKS.network), not the actual status page (LINKS.statushttps://status.nodebyte.host) that StatusBadge in this same file correctly links to. Also, it's hardcoded instead of using the already-imported LINKS constant like the rest of this file.

🔧 Proposed fix
                 { href: "/vps", label: t("footer.services.vpsServers") },
                 { href: "/games", label: t("footer.services.gameServers") },
                 { href: "/dedicated", label: t("footer.services.dedicatedServers") },
-                { href: "https://lg.nodebyte.host", label: t("footer.services.systemStatus") },
+                { href: LINKS.status, label: t("footer.services.systemStatus") },
📝 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.

                { href: "/vps", label: t("footer.services.vpsServers") },
                { href: "/games", label: t("footer.services.gameServers") },
                { href: "/dedicated", label: t("footer.services.dedicatedServers") },
                { href: LINKS.status, label: t("footer.services.systemStatus") },
🤖 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/Static/footer.tsx` around lines 122 - 125, The footer
Services entry for `t("footer.services.systemStatus")` is hardcoded to the
looking-glass URL instead of the actual status page. Update the link in `Footer`
to use the existing `LINKS.status` constant, matching the `StatusBadge` behavior
in this file and avoiding the hardcoded `https://lg.nodebyte.host` value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant