Skip to content

feat: add serverlist - #243

Open
saboooor wants to merge 12 commits into
birdflopfrom
feat/add-serverlist
Open

feat: add serverlist#243
saboooor wants to merge 12 commits into
birdflopfrom
feat/add-serverlist

Conversation

@saboooor

@saboooor saboooor commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features
    • Added a searchable Minecraft server list with filtering, sorting, rankings, live status, and verified-server options.
    • Added server submission, editing, deletion, reporting, voting, verification, and Votifier support.
    • Added detailed server pages with connection details, descriptions, plugins, tags, banners, and copyable addresses.
    • Added BBCode editing and preview tools, pagination, and formatted MOTD display.
    • Added server-linked RGB presets, plugin synchronization, and hosting indicators.
  • Security
    • Added CAPTCHA protection and validation for submissions and votes.
  • Bug Fixes
    • Improved RGB preset loading and unavailable-server handling.

Copilot AI review requested due to automatic review settings July 31, 2026 18:44
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
birdflop-com bddc927 Commit Preview URL

Branch Preview URL
Aug 01 2026, 06:05 PM

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a complete Minecraft server list with persistent schema, server submission and editing, filtering, live status checks, voting, reporting, verification, BBCode rendering, and plugin synchronization with linked servers.

Changes

Server List Platform

Layer / File(s) Summary
Server-list schema and migrations
drizzle/schema.ts, drizzle/migrations/*, drizzle/migrations/meta/*
Adds servers, votes, reports, plugin fields, verification state, hosting detection fields, relationships, indexes, and migration metadata.
Validation, status, queries, and mutations
src/util/serverlist/*, .env.example
Adds normalized input validation, BBCode rendering, live status retrieval, filtered queries, CAPTCHA verification, NuVotifier delivery, hosting detection, voting, reporting, CRUD, and moderation actions.
Server-list forms, pages, and controls
src/components/ServerList/*, src/routes/serverlist/*, src/components/Elements/*
Adds server creation and editing, listing filters, pagination, detail pages, cards, status badges, voting, reporting, verification controls, navigation, MOTD rendering, and compact copy output.
Plugin persistence and RGB server loading
src/routes/resources/plugins/index.tsx, src/routes/resources/rgb/index.tsx, src/routes/plugin@auth.ts, src/util/plugins/types.ts, src/util/dataUtils.ts, src/types.d.ts, src/components/rgbirdflop/presets/*
Shares plugin types, persists plugin state to accounts and linked servers, exposes plugins through sessions, loads server RGB presets from query parameters, and reuses shared preset and pagination helpers.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Visitor
  participant ServerList
  participant ServerStatus
  participant Database
  Visitor->>ServerList: request filtered server list
  ServerList->>Database: query servers and vote totals
  ServerList->>ServerStatus: fetch live statuses
  ServerStatus-->>ServerList: return normalized statuses
  ServerList-->>Visitor: render ranked server cards
Loading

Possibly related PRs

  • birdflop/web#193: Extends the Drizzle/D1 schema and migration system used by the server-list tables.
  • birdflop/web#201: Shares changes to useRGBCookies in src/routes/resources/rgb/index.tsx.
  • birdflop/web#223: Shares plugin state, account persistence, and linked server-plugin synchronization.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: adding the server list feature.
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 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-serverlist

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/routes/resources/plugins/index.tsx (1)

177-233: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

No debounce on writes triggered for every store mutation.

deepTrack(track, pluginsStore) reruns this task on any change to pluginsStore, including UI-only changes like switching openServer or filter. For authenticated users, each rerun now also calls setUserData and, when a server is linked, updateServerPlugins. Previously this task only wrote to localStorage, which is cheap; it now issues network requests to persist the whole store on every interaction, with no debouncing and no guard against overlapping in-flight writes if the store changes again before a previous write resolves.

Debounce the network persistence, or split UI-only state (openServer, filter) from plugin-content state so trivial interactions do not trigger a setUserData/updateServerPlugins round trip.

🤖 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 `@src/routes/resources/plugins/index.tsx` around lines 177 - 233, Debounce the
authenticated network persistence in the pluginsStore task so UI-only mutations
such as openServer or filter do not trigger immediate requests. Keep
localStorage persistence responsive, but schedule setUserData and
updateServerPlugins through a shared debounce that coalesces changes and
prevents overlapping in-flight writes, using the existing pluginsStore task and
persistence symbols.
🧹 Nitpick comments (2)
src/components/ServerList/ServerCard.tsx (1)

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

Extract the address formatting into a helper.

The nested ternary computes the Java address and the Bedrock address in one expression. src/routes/serverlist/[slug]/index.tsx lines 116-121 repeats the same rule with DEFAULT_JAVA_PORT and DEFAULT_BEDROCK_PORT. Move the logic to a shared function in src/util/serverlist/ and use the constants here too, so the two views cannot diverge.

♻️ Suggested shape
// src/util/serverlist/address.ts
export function formatAddress(
  host: string,
  port: number | null,
  defaultPort: number
): string {
  return port && port !== defaultPort ? `${host}:${port}` : host;
}
🤖 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 `@src/components/ServerList/ServerCard.tsx` around lines 53 - 65, Extract the
nested address-formatting logic from the ServerCard displayIp calculation into a
shared formatAddress helper under src/util/serverlist/, and reuse it for both
Java and Bedrock addresses. Update ServerCard and the server-list route to use
DEFAULT_JAVA_PORT and DEFAULT_BEDROCK_PORT through this helper, preserving
omission of default ports and formatting only the selected primary host.
src/routes/serverlist/[slug]/index.tsx (1)

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

Reuse Output for the copy control, and handle the clipboard rejection.

navigator.clipboard?.writeText(address) returns an unhandled promise. A denied clipboard permission fails silently, and the user gets no feedback. src/components/Elements/Output.tsx already implements copy with notifications. Use that component here instead of the local button.

🤖 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 `@src/routes/serverlist/`[slug]/index.tsx around lines 90 - 111, Replace the
local copy button in ConnectRow with the existing Output component, passing
address as its displayed/copyable value and preserving the current row styling
and label. Remove the direct navigator.clipboard call so clipboard failures are
handled by Output’s notification behavior.
🤖 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 `@drizzle/schema.ts`:
- Around line 159-223: Prevent votifierToken from reaching clients by replacing
whole-row selections of servers in the server-list query flow and the server
detail route with explicit public-column projections. Exclude votifierToken from
those projections while preserving all fields required by the listing UI; retain
access only in privileged backend paths that need to deliver votes.

In `@src/components/Elements/Output.tsx`:
- Around line 38-52: Update the compact branch in the Output component to use a
keyboard-accessible button with button semantics while preserving the existing
click behavior, styling, value, and copy icon. Keep the control labeled with the
translated “Copy IP” title by routing that string through inlineTranslate,
consistent with the other localized strings in the component.
- Around line 23-36: Update the onClick handler in Output.tsx so
notifications.push(notification.toJSON()) runs after
navigator.clipboard.writeText(value) resolves or rejects, ensuring the
catch-updated failure notification snapshot is pushed. Preserve the green
success notification for successful copies and the existing error details and
red styling for failures.

In `@src/components/ServerList/StatusBadge.tsx`:
- Around line 13-39: Update the players display condition in the StatusBadge
component so it narrows status before accessing player data, and ensure the
status.players.online and status.players.max reads remain safe when status or
players is null. Preserve the existing rendering behavior for online servers
with showPlayers enabled.

In `@src/components/ServerList/Turnstile.tsx`:
- Around line 57-76: Update the Turnstile effect around render and cleanup so
the script element and load-handler state are accessible to ctx.cleanup. Remove
the load listener during cleanup and guard render with an active-mounted flag,
ensuring the script cannot create a widget after unmount while preserving normal
immediate and script-load rendering.

In `@src/routes/resources/plugins/index.tsx`:
- Around line 134-152: Update the database plugin-state condition in the
useVisibleTask$ initialization flow to distinguish missing data from an
intentionally empty sync. Treat any defined dbPlugins value, including one with
an empty servers object, as authoritative and restore its stored fields; only
enter the localStorage migration/setUserData fallback when dbPlugins itself is
undefined.

In `@src/routes/serverlist/`[slug]/index.tsx:
- Around line 72-76: Annotate the session value retrieved in the
server-management checks as Session | null, using the existing Session type from
src/util/dataUtils.ts. Apply this in src/routes/serverlist/[slug]/index.tsx
lines 72-76 and src/routes/serverlist/[slug]/edit/index.tsx lines 21-24 before
accessing session.user, without changing the canManage logic.

In `@src/routes/serverlist/index.tsx`:
- Around line 76-86: Update the error handling in the server-loading function to
log the caught error server-side, but replace error: String(err) with a fixed
user-safe message before the result reaches the page rendering at line 356. Keep
the existing empty rows, total, statuses, liveRefined, and params behavior
unchanged.
- Around line 31-35: Validate the numeric results used to derive page and
perPage in the server-list pagination setup. Update the page and perPage parsing
expressions so non-numeric query values fall back to their existing defaults
before applying the Math.max/Math.min bounds, ensuring valid values reach limit,
offset, and totalPages.

In `@src/util/serverlist/actions.ts`:
- Around line 271-301: Update reportServer to validate a Turnstile token using
the existing verifyTurnstile pattern from voteForServer, and enforce the same
24-hour username/IP cooldown before inserting into serverReports. Preserve the
existing validation and response behavior, and reject submissions that fail
CAPTCHA or rate-limit checks before the database insert.
- Around line 352-370: Update setServerVerified to look up the server by
serverId before performing the update, following the existing
updateServerPlugins pattern. Return the established not-found failure response
when no matching server exists, and only return success after updating an
existing row.
- Around line 47-370: Add a shared session accessor that casts
sharedMap.get('session') to Session | null, then replace the direct session
lookups in createServer, updateServer, deleteServer, reportServer, and
updateServerPlugins with this helper. Import Session from `@auth/qwik` and
preserve the existing null-safe authorization behavior.
- Line 88: Standardize all isAdmin invocations in the caller actions across the
serverlist and data-utils modules. Use the bound server$ call form consistently
so each admin check receives the request context and can access this.sharedMap,
including the invocation near the existing const admin assignment.

In `@src/util/serverlist/bbcode.ts`:
- Around line 73-105: Type every regex replacement callback parameter currently
inferred as any in the BBCode conversion chain, including color/size, URL
href/label, image src, and list callback parameters, as string. Update the
callbacks around the existing safeColor, safeSize, safeUrl, and related list
handling without changing their behavior.

In `@src/util/serverlist/queries.ts`:
- Around line 96-120: Prevent sensitive server and owner fields from reaching
public Qwik loader payloads: in src/util/serverlist/queries.ts lines 96-120 and
src/routes/serverlist/[slug]/index.tsx lines 36-88, replace full-row selections
with explicit public column projections excluding servers.votifierToken and
owner PII; in src/routes/serverlist/[slug]/edit/index.tsx lines 12-27, only
return listing data when canManage is true and exclude votifierToken; in
drizzle/schema.ts lines 159-223, either move Votifier credentials into a
separate table or document the invariant that votifierToken is never selected by
public read paths.

In `@src/util/serverlist/status.ts`:
- Around line 57-67: Update the fetch call in getServerStatus to pass an
AbortSignal.timeout using the appropriate request duration, and ensure an
aborted request follows the existing fallback path. Preserve the current
response validation and McStatusResponse handling for requests that complete
normally.

In `@src/util/serverlist/turnstile.ts`:
- Around line 27-44: Update the Turnstile verification fetch in verifyTurnstile
to use an AbortController with a 5-second timeout, passing its signal to fetch
and aborting when the timer expires. Clear the timeout after the fetch completes
or fails, while preserving the existing success, failure, and catch handling.

In `@src/util/serverlist/validation.ts`:
- Around line 75-82: Update parsePort to validate string inputs after trimming,
requiring the complete value to consist only of decimal digits before
conversion. Replace the partial parseInt behavior with Number() after this
check, while preserving existing undefined, empty, null, integer, and 1–65535
range handling.
- Around line 94-95: Update server destination validation around HOST_RE to
resolve hostnames and reject private or reserved IP ranges, including localhost,
loopback, RFC 1918, link-local, unspecified, and equivalent IPv6 addresses,
before any status check or Votifier message is sent. Ensure status.ts validates
the resolved destination before fetch() and sendVotifierV2() performs the same
protection before creating the cloudflare:sockets TCP connection.

In `@src/util/serverlist/votifier.ts`:
- Around line 75-91: Update the socket declaration and the dynamically imported
connect return type in the votifier flow to use ReadableStream<Uint8Array> and
WritableStream<Uint8Array> instead of unparameterized stream types, ensuring
reader.read().value is typed for TextDecoder.decode without call-site casts.

---

Outside diff comments:
In `@src/routes/resources/plugins/index.tsx`:
- Around line 177-233: Debounce the authenticated network persistence in the
pluginsStore task so UI-only mutations such as openServer or filter do not
trigger immediate requests. Keep localStorage persistence responsive, but
schedule setUserData and updateServerPlugins through a shared debounce that
coalesces changes and prevents overlapping in-flight writes, using the existing
pluginsStore task and persistence symbols.

---

Nitpick comments:
In `@src/components/ServerList/ServerCard.tsx`:
- Around line 53-65: Extract the nested address-formatting logic from the
ServerCard displayIp calculation into a shared formatAddress helper under
src/util/serverlist/, and reuse it for both Java and Bedrock addresses. Update
ServerCard and the server-list route to use DEFAULT_JAVA_PORT and
DEFAULT_BEDROCK_PORT through this helper, preserving omission of default ports
and formatting only the selected primary host.

In `@src/routes/serverlist/`[slug]/index.tsx:
- Around line 90-111: Replace the local copy button in ConnectRow with the
existing Output component, passing address as its displayed/copyable value and
preserving the current row styling and label. Remove the direct
navigator.clipboard call so clipboard failures are handled by Output’s
notification behavior.
🪄 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: 3b7489f4-3bea-4f91-8879-3d5c06bafeb4

📥 Commits

Reviewing files that changed from the base of the PR and between ef15483 and f00f425.

📒 Files selected for processing (43)
  • drizzle/migrations/0006_reflective_tarantula.sql
  • drizzle/migrations/0007_burly_madame_web.sql
  • drizzle/migrations/0008_safe_nightshade.sql
  • drizzle/migrations/0009_soft_krista_starr.sql
  • drizzle/migrations/0010_stale_sumo.sql
  • drizzle/migrations/0011_wet_stranger.sql
  • drizzle/migrations/meta/0006_snapshot.json
  • drizzle/migrations/meta/0007_snapshot.json
  • drizzle/migrations/meta/0008_snapshot.json
  • drizzle/migrations/meta/0009_snapshot.json
  • drizzle/migrations/meta/0010_snapshot.json
  • drizzle/migrations/meta/0011_snapshot.json
  • drizzle/migrations/meta/_journal.json
  • drizzle/schema.ts
  • src/components/Elements/Nav.tsx
  • src/components/Elements/Output.tsx
  • src/components/ServerList/BBCodeEditor.tsx
  • src/components/ServerList/ServerCard.tsx
  • src/components/ServerList/ServerControls.tsx
  • src/components/ServerList/ServerForm.tsx
  • src/components/ServerList/StatusBadge.tsx
  • src/components/ServerList/Turnstile.tsx
  • src/components/ServerList/VoteSection.tsx
  • src/components/rgbirdflop/presets/MyPrivatePresets.tsx
  • src/routes/plugin@auth.ts
  • src/routes/resources/plugins/index.tsx
  • src/routes/resources/rgb/index.tsx
  • src/routes/serverlist/[slug]/edit/index.tsx
  • src/routes/serverlist/[slug]/index.tsx
  • src/routes/serverlist/index.tsx
  • src/routes/serverlist/submit/index.tsx
  • src/types.d.ts
  • src/util/dataUtils.ts
  • src/util/plugins/types.ts
  • src/util/serverlist/__tests__/validation.test.ts
  • src/util/serverlist/actions.ts
  • src/util/serverlist/bbcode.ts
  • src/util/serverlist/constants.ts
  • src/util/serverlist/queries.ts
  • src/util/serverlist/status.ts
  • src/util/serverlist/turnstile.ts
  • src/util/serverlist/validation.ts
  • src/util/serverlist/votifier.ts

Comment thread drizzle/schema.ts
Comment on lines +23 to +36
const onClick = () => {
const notification = new Notification()
.setTitle(copiedTitle)
.setDescription(copiedDescription)
.setBgColor('lum-grad-bg-green/50');
navigator.clipboard.writeText(value).catch((err: unknown) => {
notification
.setTitle(copyFailedTitle)
.setDescription(err instanceof Error ? err.message : String(err))
.setBgColor('lum-grad-bg-red/50')
.setPersist(true);
});
notifications.push(notification.toJSON());
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The copy-failure notification never reaches the user.

notifications.push(notification.toJSON()) runs synchronously and pushes a plain snapshot object. The catch callback runs later and mutates the notification instance, not the pushed snapshot. On a clipboard failure the user still sees the green "copied" notification. Push the notification inside the promise resolution instead.

🐛 Proposed fix
   const onClick = () => {
     const notification = new Notification()
       .setTitle(copiedTitle)
       .setDescription(copiedDescription)
       .setBgColor('lum-grad-bg-green/50');
-    navigator.clipboard.writeText(value).catch((err: unknown) => {
-      notification
-        .setTitle(copyFailedTitle)
-        .setDescription(err instanceof Error ? err.message : String(err))
-        .setBgColor('lum-grad-bg-red/50')
-        .setPersist(true);
-    });
-    notifications.push(notification.toJSON());
+    navigator.clipboard
+      .writeText(value)
+      .then(() => notifications.push(notification.toJSON()))
+      .catch((err: unknown) => {
+        notifications.push(
+          notification
+            .setTitle(copyFailedTitle)
+            .setDescription(err instanceof Error ? err.message : String(err))
+            .setBgColor('lum-grad-bg-red/50')
+            .setPersist(true)
+            .toJSON()
+        );
+      });
   };
📝 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
const onClick = () => {
const notification = new Notification()
.setTitle(copiedTitle)
.setDescription(copiedDescription)
.setBgColor('lum-grad-bg-green/50');
navigator.clipboard.writeText(value).catch((err: unknown) => {
notification
.setTitle(copyFailedTitle)
.setDescription(err instanceof Error ? err.message : String(err))
.setBgColor('lum-grad-bg-red/50')
.setPersist(true);
});
notifications.push(notification.toJSON());
};
const onClick = () => {
const notification = new Notification()
.setTitle(copiedTitle)
.setDescription(copiedDescription)
.setBgColor('lum-grad-bg-green/50');
navigator.clipboard
.writeText(value)
.then(() => notifications.push(notification.toJSON()))
.catch((err: unknown) => {
notifications.push(
notification
.setTitle(copyFailedTitle)
.setDescription(err instanceof Error ? err.message : String(err))
.setBgColor('lum-grad-bg-red/50')
.setPersist(true)
.toJSON()
);
});
};
🤖 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 `@src/components/Elements/Output.tsx` around lines 23 - 36, Update the onClick
handler in Output.tsx so notifications.push(notification.toJSON()) runs after
navigator.clipboard.writeText(value) resolves or rejects, ensuring the
catch-updated failure notification snapshot is pushed. Preserve the green
success notification for successful copies and the existing error details and
red styling for failures.

Comment thread src/components/Elements/Output.tsx
Comment on lines +13 to +39
const online = !!status?.online;
return (
<div class="flex items-center gap-2 text-sm">
<span
class={{
'flex items-center gap-1.5 font-semibold': true,
'text-green-400': online,
'text-lum-text-secondary': !online,
}}
>
<Circle
size={10}
class={
online
? 'fill-green-400 text-green-400'
: 'fill-lum-text-secondary text-lum-text-secondary'
}
/>
{online ? 'Online' : 'Offline'}
</span>
{online && showPlayers && (
<span class="text-lum-text-secondary flex items-center gap-1.5">
<Users size={14} />
{status.players.online.toLocaleString()}
<span class="opacity-50">
/ {status.players.max.toLocaleString()}
</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -H -t f 'tsconfig*.json' --exec sh -c 'echo "== $1"; cat "$1"' _ {}

Repository: birdflop/web

Length of output: 2068


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -H 'StatusBadge.tsx' .

echo "== file outline =="
ast-grep outline src/components/ServerList/StatusBadge.tsx --view expanded || true

echo "== relevant file contents =="
cat -n src/components/ServerList/StatusBadge.tsx

echo "== search ServerStatus definition =="
rg -n "ServerStatus|type .*Status|interface .*Status" src packages -g '*.ts' -g '*.tsx' || true

Repository: birdflop/web

Length of output: 3579


🌐 Web query:

TypeScript strictNullChecks boolean variable narrow null inside conditional non-null assertion

💡 Result:

When strictNullChecks is enabled, TypeScript uses control flow-based type analysis to narrow variables [1]. When you perform a conditional check on a variable that might be null or undefined (e.g., if (variable)), TypeScript narrows the type within that block based on truthiness [2][3][4]. To safely handle a variable that could be null or undefined, you should prefer explicit type guards over non-null assertions whenever possible [5][6]. 1. Truthiness Narrowing: Using if (variable) will narrow the type by removing null and undefined, as they are falsy values [2][7][4]. However, be cautious if your variable could be an empty string or the number 0, as these are also falsy and would be removed by this check [2]. 2. Explicit Null Checks: Using if (variable!== null && variable!== undefined) or the more concise if (variable!= null) is the recommended way to explicitly guard against null and undefined [5][4]. This is safe, clear, and specifically targets the nullability issue without inadvertently excluding other valid values [5]. 3. Non-null Assertion Operator: The non-null assertion operator (!) is a postfix operator that tells the compiler to treat a value as non-null/non-undefined [5][6]. It does not perform a runtime check and is essentially an assertion by the developer [5][6]. Because it bypasses type checking, it should only be used when you are absolutely certain the value is present and the compiler cannot infer it [5][6]. Using it inside a conditional to "fix" a type error is generally discouraged because it creates a risk of runtime errors if your assumption is wrong [6]. In summary, instead of using a non-null assertion inside a conditional, you should use an explicit null check or a type guard to allow TypeScript to correctly narrow the type for you [5][4]. This leverages the language's safety features rather than overriding them [5].

Citations:


Narrow status before reading its players.

online is a boolean, so {online && showPlayers && ...} does not narrow status from ServerStatus | null; status.players can still report as possibly null. Use optional chaining in the conditional and on the reads, or narrow status before the block.

🔧 Proposed fix
-        {online && showPlayers && (
+        {status?.online && showPlayers && (
           <span class="text-lum-text-secondary flex items-center gap-1.5">
             <Users size={14} />
             {status.players.online.toLocaleString()}
             <span class="opacity-50">
-              / {status.players.max.toLocaleString()}
+              / {status.max.toLocaleString()}
🤖 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 `@src/components/ServerList/StatusBadge.tsx` around lines 13 - 39, Update the
players display condition in the StatusBadge component so it narrows status
before accessing player data, and ensure the status.players.online and
status.players.max reads remain safe when status or players is null. Preserve
the existing rendering behavior for online servers with showPlayers enabled.

Comment on lines +57 to +76
if (window.turnstile) {
render();
} else {
// Load the script once; reuse it across widgets.
let script = document.querySelector<HTMLScriptElement>(
`script[src="${SCRIPT_SRC}"]`
);
if (!script) {
script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
document.head.appendChild(script);
}
script.addEventListener('load', render);
}

ctx.cleanup(() => {
if (widgetId && window.turnstile) window.turnstile.remove(widgetId);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Remove the load listener on cleanup, and skip rendering after unmount.

If the component unmounts before the Turnstile script loads, the load listener still fires and calls render(). The widget is then created after ctx.cleanup ran, so window.turnstile.remove is never called for it. The listener also keeps render and the container reference alive.

🔧 Proposed fix
     let widgetId: string | undefined;
+    let disposed = false;
 
     const render = () => {
-      if (!window.turnstile || !containerRef.value) return;
+      if (disposed || !window.turnstile || !containerRef.value) return;
       widgetId = window.turnstile.render(containerRef.value, {
@@
-      script.addEventListener('load', render);
+      script.addEventListener('load', render, { once: true });
     }
 
     ctx.cleanup(() => {
+      disposed = true;
+      script?.removeEventListener('load', render);
       if (widgetId && window.turnstile) window.turnstile.remove(widgetId);
     });

Declare script outside the else branch so cleanup can reference it.

🤖 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 `@src/components/ServerList/Turnstile.tsx` around lines 57 - 76, Update the
Turnstile effect around render and cleanup so the script element and
load-handler state are accessible to ctx.cleanup. Remove the load listener
during cleanup and guard render with an active-mounted flag, ensuring the script
cannot create a widget after unmount while preserving normal immediate and
script-load rendering.

Comment on lines +57 to +67
try {
const res = await fetch(url, {
headers: { 'User-Agent': 'birdflop.com server list' },
// Cloudflare-specific edge caching so repeated views are cheap and we
// stay friendly to mcstatus.io rate limits.
cf: { cacheTtl: CACHE_TTL_SECONDS, cacheEverything: true },
});

if (!res.ok) return fallback;

const data: McStatusResponse = await res.json();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a timeout to the mcstatus.io request.

fetch has no abort signal. src/util/serverlist/queries.ts calls getServerStatus for up to CANDIDATE_CAP (75) servers per page view. If mcstatus.io is slow, every list request blocks until the platform limit, and page rendering stalls. Add AbortSignal.timeout and treat the abort as fallback.

🔧 Proposed fix
+const REQUEST_TIMEOUT_MS = 5000;
+
     const res = await fetch(url, {
       headers: { 'User-Agent': 'birdflop.com server list' },
+      signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
       // Cloudflare-specific edge caching so repeated views are cheap and we
       // stay friendly to mcstatus.io rate limits.
       cf: { cacheTtl: CACHE_TTL_SECONDS, cacheEverything: true },
     });
📝 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
try {
const res = await fetch(url, {
headers: { 'User-Agent': 'birdflop.com server list' },
// Cloudflare-specific edge caching so repeated views are cheap and we
// stay friendly to mcstatus.io rate limits.
cf: { cacheTtl: CACHE_TTL_SECONDS, cacheEverything: true },
});
if (!res.ok) return fallback;
const data: McStatusResponse = await res.json();
try {
const REQUEST_TIMEOUT_MS = 5000;
const res = await fetch(url, {
headers: { 'User-Agent': 'birdflop.com server list' },
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
// Cloudflare-specific edge caching so repeated views are cheap and we
// stay friendly to mcstatus.io rate limits.
cf: { cacheTtl: CACHE_TTL_SECONDS, cacheEverything: true },
});
if (!res.ok) return fallback;
const data: McStatusResponse = await res.json();
🤖 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 `@src/util/serverlist/status.ts` around lines 57 - 67, Update the fetch call in
getServerStatus to pass an AbortSignal.timeout using the appropriate request
duration, and ensure an aborted request follows the existing fallback path.
Preserve the current response validation and McStatusResponse handling for
requests that complete normally.

Comment on lines +27 to +44
try {
const body = new FormData();
body.append('secret', secret);
body.append('response', token);
if (remoteIp) body.append('remoteip', remoteIp);

const res = await fetch(VERIFY_URL, { method: 'POST', body });
const data: TurnstileVerifyResponse = await res.json();

if (data.success) return { success: true };
return {
success: false,
error: `CAPTCHA verification failed${data['error-codes']?.length ? `: ${data['error-codes'].join(', ')}` : ''}`,
};
} catch (err) {
console.error('Turnstile verification error:', err);
return { success: false, error: 'CAPTCHA verification error' };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching turnstile/votifier:"
fd -a 'turnstile\.ts|votifier\.ts' . | sed 's#^\./##'

echo
echo "Relevant snippets:"
for f in $(fd 'turnstile\.ts|votifier\.ts' .); do
  echo "--- $f ($(wc -l < "$f") lines) ---"
  sed -n '1,220p' "$f"
done

echo
echo "Search for withTimeout and turnstile verify:"
rg -n "withTimeout|verifyTurnstile|TurnstileVerifyResponse|VERIFY_URL|voteForServer|challenges.cloudflare.com|turnstile" . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**'

Repository: birdflop/web

Length of output: 11148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

targets = sorted(Path('.').glob('**/*.ts'))
turnstile_code = None
for path in targets:
    text = path.read_text(errors='ignore')
    if path.as_posix().endswith('src/util/serverlist/turnstile.ts'):
        turnstile_code = text
        continue

if not turnstile_code:
    print("turnstile.ts not found")
    raise SystemExit

checks = {
    "fetch_without_signal": "const res = await fetch(VERIFY_URL, { method: 'POST', body });" in turnstile_code,
    "fetch_has_signal": bool(re.search(r"fetch\(VERIFY_URL\s*,\s*{[\s\S]*?signal:", turnstile_code)),
    "verify_returns_on_success": "if (data.success) return { success: true };" in turnstile_code,
    "returns_success_false_on_failure": "return { success: false, error:" in turnstile_code,
}
for k, v in checks.items():
    print(f"{k}={v}")
PY

Repository: birdflop/web

Length of output: 268


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- src/util/serverlist/actions.ts lines 150-190 ---"
sed -n '150,190p' src/util/serverlist/actions.ts

echo
echo "--- package/dev tool clues ---"
for f in package.json vite.config.* qwik*.json .github/workflows/*; do
  [ -f "$f" ] && echo "### $f" && sed -n '1,220p' "$f"
done

Repository: birdflop/web

Length of output: 16662


Add a timeout to the Turnstile verification fetch.

voteForServer awaits verifyTurnstile() on the server action request path. If challenges.cloudflare.com/turnstile/v0/siteverify does not respond, this fetch can hold the vote request until the outer platform timeout runs and no Votifier delivery is attempted.

src/util/serverlist/votifier.ts already defines a 5s timeout for external socket operations; use the same timeout discipline for this HTTP call, including AbortController.abort() plus clearing the timer.

🤖 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 `@src/util/serverlist/turnstile.ts` around lines 27 - 44, Update the Turnstile
verification fetch in verifyTurnstile to use an AbortController with a 5-second
timeout, passing its signal to fetch and aborting when the timer expires. Clear
the timeout after the fetch completes or fails, while preserving the existing
success, failure, and catch handling.

Comment on lines +75 to +82
function parsePort(
value: string | number | undefined
): number | null | undefined {
if (value === undefined || value === '' || value === null) return null;
const n = typeof value === 'number' ? value : parseInt(value, 10);
if (!Number.isInteger(n)) return undefined; // signal "invalid"
if (n < 1 || n > 65535) return undefined;
return n;

Copy link
Copy Markdown
Contributor

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

Reject port strings with trailing non-numeric characters.

Line 79 accepts 25565abc as 25565 and 1.5 as 1. validateServerInput is the persistence boundary, so malformed input is stored as a different port. Require the complete trimmed string to match decimal digits before converting it with Number().

Proposed fix
-  if (value === undefined || value === '' || value === null) return null;
-  const n = typeof value === 'number' ? value : parseInt(value, 10);
+  if (value === undefined || value === null) return null;
+  if (typeof value === 'string' && value.trim() === '') return null;
+  if (typeof value === 'string' && !/^\d+$/.test(value.trim()))
+    return undefined;
+  const n = typeof value === 'number' ? value : Number(value.trim());
📝 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
function parsePort(
value: string | number | undefined
): number | null | undefined {
if (value === undefined || value === '' || value === null) return null;
const n = typeof value === 'number' ? value : parseInt(value, 10);
if (!Number.isInteger(n)) return undefined; // signal "invalid"
if (n < 1 || n > 65535) return undefined;
return n;
function parsePort(
value: string | number | undefined
): number | null | undefined {
if (value === undefined || value === null) return null;
if (typeof value === 'string' && value.trim() === '') return null;
if (typeof value === 'string' && !/^\d+$/.test(value.trim()))
return undefined;
const n = typeof value === 'number' ? value : Number(value.trim());
if (!Number.isInteger(n)) return undefined; // signal "invalid"
if (n < 1 || n > 65535) return undefined;
return n;
🤖 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 `@src/util/serverlist/validation.ts` around lines 75 - 82, Update parsePort to
validate string inputs after trimming, requiring the complete value to consist
only of decimal digits before conversion. Replace the partial parseInt behavior
with Number() after this check, while preserving existing undefined, empty,
null, integer, and 1–65535 range handling.

Comment on lines +94 to +95
// Hostnames: allow domain names and IPv4 (no scheme, no path).
const HOST_RE = /^[a-zA-Z0-9.-]+$/;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/util/serverlist/status.ts --items all
ast-grep outline src/util/serverlist/votifier.ts --items all

rg -n -C 3 --type ts \
  'fetch\s*\(|https?\.request|net\.connect|createConnection|dgram|javaHost|bedrockHost|votifierHost' \
  src/util/serverlist/status.ts \
  src/util/serverlist/votifier.ts \
  src/util/serverlist/actions.ts

Repository: birdflop/web

Length of output: 3850


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)validation\.ts$|(^|/)status\.ts$|(^|/)votifier\.ts$|(^|/)actions\.ts$' || true

echo "== validation outline and relevant lines =="
sed -n '1,150p' src/util/serverlist/validation.ts

echo "== status.ts =="
cat -n src/util/serverlist/status.ts

echo "== votifier.ts =="
cat -n src/util/serverlist/votifier.ts

echo "== status/votifier imports and usages =="
rg -n -C 2 'validate.*host|host.*valid|validation|fetchStatus|sendVotifierV2|javaHost|bedrockHost|votifierHost|localhost|127\.0\.0\.1|private|reserved' src || true

Repository: birdflop/web

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const HOST_RE = /^[a-zA-Z0-9.-]+$/;
const values = ['localhost', '127.0.0.1', '192.168.1.10', '10.0.0.1', '172.16.0.1', '169.254.1.1', '0.0.0.0'];
for (const v of values) {
  console.log(v, HOST_RE.test(v));
}

try {
  console.log('dns.lookup localhost', JSON.stringify(require('node:dns').lookupSync('localhost', { family: 0 }), null, 2));
} catch (e) {
  console.log('dns.resolve localhost', JSON.stringify(require('node:dns').resolveSync('localhost'), null, 2));
}
JS

Repository: birdflop/web

Length of output: 1018


Block private or reserved destinations before checking status or sending Votifier messages.

HOST_RE permits localhost, 127.0.0.1, private RFC 1918 ranges, link-local addresses, and 0.0.0.0. status.ts passes those values to fetch() for mcstatus, while sendVotifierV2() opens cloudflare:sockets using the stored Votifier host. Resolve hostnames before checking status, reject private/reserved destinations, and block reserved IPs before creating the Votifier TCP connection.

🤖 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 `@src/util/serverlist/validation.ts` around lines 94 - 95, Update server
destination validation around HOST_RE to resolve hostnames and reject private or
reserved IP ranges, including localhost, loopback, RFC 1918, link-local,
unspecified, and equivalent IPv6 addresses, before any status check or Votifier
message is sent. Ensure status.ts validates the resolved destination before
fetch() and sendVotifierV2() performs the same protection before creating the
cloudflare:sockets TCP connection.

Comment on lines +75 to +91
let socket:
| {
readable: ReadableStream;
writable: WritableStream;
close: () => Promise<void>;
}
| undefined;

try {
// Dynamically imported so this module never gets pulled into a client bundle.
const { connect } = (await import('cloudflare:sockets')) as {
connect: (addr: { hostname: string; port: number }) => {
readable: ReadableStream;
writable: WritableStream;
close: () => Promise<void>;
};
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Type the socket streams to fix the no-unsafe-argument warnings.

socket.readable/socket.writable and the connect return type use bare ReadableStream/WritableStream, so reader.read() returns any for .value. That any value later flows into TextDecoder.decode() at Lines 108 and 142-144, which is what CI flags.

Type the streams as ReadableStream<Uint8Array>/WritableStream<Uint8Array> here, at the declaration sites, instead of casting at each call site.

🔧 Proposed fix
   let socket:
     | {
-        readable: ReadableStream;
-        writable: WritableStream;
+        readable: ReadableStream<Uint8Array>;
+        writable: WritableStream<Uint8Array>;
         close: () => Promise<void>;
       }
     | undefined;
     const { connect } = (await import('cloudflare:sockets')) as {
       connect: (addr: { hostname: string; port: number }) => {
-        readable: ReadableStream;
-        writable: WritableStream;
+        readable: ReadableStream<Uint8Array>;
+        writable: WritableStream<Uint8Array>;
         close: () => Promise<void>;
       };
     };

As per pipeline failures, "vp check reported TypeScript no-unsafe-argument: values of type any are passed to TextDecoder.decode."

📝 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
let socket:
| {
readable: ReadableStream;
writable: WritableStream;
close: () => Promise<void>;
}
| undefined;
try {
// Dynamically imported so this module never gets pulled into a client bundle.
const { connect } = (await import('cloudflare:sockets')) as {
connect: (addr: { hostname: string; port: number }) => {
readable: ReadableStream;
writable: WritableStream;
close: () => Promise<void>;
};
};
let socket:
| {
readable: ReadableStream<Uint8Array>;
writable: WritableStream<Uint8Array>;
close: () => Promise<void>;
}
| undefined;
try {
// Dynamically imported so this module never gets pulled into a client bundle.
const { connect } = (await import('cloudflare:sockets')) as {
connect: (addr: { hostname: string; port: number }) => {
readable: ReadableStream<Uint8Array>;
writable: WritableStream<Uint8Array>;
close: () => Promise<void>;
};
};
🤖 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 `@src/util/serverlist/votifier.ts` around lines 75 - 91, Update the socket
declaration and the dynamically imported connect return type in the votifier
flow to use ReadableStream<Uint8Array> and WritableStream<Uint8Array> instead of
unparameterized stream types, ensuring reader.read().value is typed for
TextDecoder.decode without call-site casts.

Source: Pipeline failures

Auto-detect listings hosted on Birdflop by resolving their address
(DNS-over-HTTPS, incl. Java SRV) against panel node/allocation IPs
pulled from the Pterodactyl Application API and cached in D1 - no
hardcoded IPs, nothing exposed to clients beyond a boolean badge.
Re-checked on edit and lazily once a day on detail views.

Also fixes issues found while auditing the feature:
- strip Votifier host/port/token from public loader payloads (the
  token allowed forging votes straight to a server's NuVotifier port)
- gate the edit loader so non-owners no longer receive the full row
- return only name/image for listing owners instead of the whole user
  row (leaked emails)
- require http(s) URLs for the Discord link (javascript: URI XSS)
- set createdAt explicitly on inserts; the CURRENT_TIMESTAMP default
  stores text, making vote cooldowns permanent and monthly counts
  all-time (text sorts above numbers in SQLite)
- cap listings at 10 per account, one report per server per user/IP
  per 24h, and guard against NaN page params
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 1, 2026

Copy link
Copy Markdown

Deploying web with  Cloudflare Pages  Cloudflare Pages

Latest commit: bddc927
Status:🚫  Build failed.

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 @.env.example:
- Line 11: Add a trailing blank line after the PANEL_URL entry in the
environment template so the file ends with the required blank line and satisfies
dotenv-linter’s EndingBlankLine check.

In `@drizzle/migrations/meta/0012_snapshot.json`:
- Line 1: Run vp check --fix to regenerate the formatting for the
0012_snapshot.json generated snapshot, then include the resulting file changes
in the commit.

In `@src/util/serverlist/birdflop.ts`:
- Around line 50-53: Update both dohQuery() and fetchPanelIps() to apply the
project’s existing abort/timeout policy to their external fetch calls, ensuring
neither request can remain unbounded. Preserve fetchPanelIps()’s stale IP-cache
fallback when the timed request fails.
🪄 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: 300fe797-29d6-4152-8e7f-98585e41f012

📥 Commits

Reviewing files that changed from the base of the PR and between f00f425 and cf3b153.

📒 Files selected for processing (14)
  • .env.example
  • drizzle/migrations/0012_black_demogoblin.sql
  • drizzle/migrations/meta/0012_snapshot.json
  • drizzle/migrations/meta/_journal.json
  • drizzle/schema.ts
  • src/components/ServerList/ServerCard.tsx
  • src/routes/serverlist/[slug]/edit/index.tsx
  • src/routes/serverlist/[slug]/index.tsx
  • src/routes/serverlist/index.tsx
  • src/util/serverlist/actions.ts
  • src/util/serverlist/birdflop.ts
  • src/util/serverlist/constants.ts
  • src/util/serverlist/queries.ts
  • src/util/serverlist/validation.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/util/serverlist/constants.ts
  • src/routes/serverlist/[slug]/edit/index.tsx
  • src/routes/serverlist/index.tsx
  • src/util/serverlist/actions.ts
  • src/util/serverlist/validation.ts
  • src/routes/serverlist/[slug]/index.tsx
  • src/components/ServerList/ServerCard.tsx

Comment thread .env.example
# on the server list. Optional; detection is skipped when unset.
PANEL_API_KEY=
# Defaults to https://panel.birdflop.com when unset.
PANEL_URL= No newline at end of file

Copy link
Copy Markdown
Contributor

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

Add the required ending blank line.

Add a blank line after PANEL_URL=. dotenv-linter reports EndingBlankLine for this file.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 11-11: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🤖 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 at line 11, Add a trailing blank line after the PANEL_URL entry
in the environment template so the file ends with the required blank line and
satisfies dotenv-linter’s EndingBlankLine check.

Source: Linters/SAST tools

Comment thread drizzle/migrations/meta/0012_snapshot.json
Comment on lines +50 to +53
const res = await fetch(
`${DOH_URL}?name=${encodeURIComponent(name)}&type=${type}`,
{ headers: { accept: 'application/dns-json' } }
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## repository files matching birdflop.ts"
fd -a 'birdflop\.ts$' . || true

echo
echo "## outline"
ast-grep outline src/util/serverlist/birdflop.ts --view expanded || true

echo
echo "## relevant file sections"
wc -l src/util/serverlist/birdflop.ts
sed -n '1,230p' src/util/serverlist/birdflop.ts | cat -n

echo
echo "## fetch/timeout/policy references"
rg -n "fetch\\(|RequestTimeout|timeout|AbortSignal|AbortController|setTimeout|Promise.race|shared|policy|DOH_URL|apiKey|panelUrl" src || true

Repository: birdflop/web

Length of output: 22749


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## caller sites in birdflop.ts"
sed -n '204,350p' src/util/serverlist/birdflop.ts | cat -n

echo
echo "## actions usages"
sed -n '30,90p' src/util/serverlist/actions.ts | cat -n
rg -n "detectBirdflopHosted|refreshBirdflopHosted|isBirdflopHosted|birdflop" src/util/serverlist/actions.ts src/routes src/components -g '*.ts' -g '*.tsx' | head -n 120

Repository: birdflop/web

Length of output: 18483


🌐 Web query:

Cloudflare Workers request limit time budget timeout abortController fetch documentation

💡 Result:

Cloudflare Workers support the standard Fetch API and AbortController/AbortSignal for managing request timeouts [1][2]. ### Fetch Timeouts with AbortController To implement a timeout for a fetch request in a Cloudflare Worker, you use an AbortController to signal the cancellation of the fetch operation [1][2]. Example implementation: javascript export default { async fetch(request) { const controller = new AbortController(); // Set a timeout of 5000 milliseconds const timeoutId = setTimeout(() => controller.abort(), 5000); try { const response = await fetch("https://example.com", { signal: controller.signal }); return response; } catch (err) { if (err.name === 'AbortError') { return new Response("Request timed out", { status: 504 }); } throw err; } finally { clearTimeout(timeoutId); } } }; ### Key Considerations 1. CPU Time vs. Wall Time: Workers have strict CPU time limits (e.g., 10ms for free tier, 30s-5m for paid tiers) [3][4]. Importantly, time spent waiting for network requests (including fetch calls) does not count toward CPU time [3]. 2. Wall Time Limits: While there is no hard "wall time" limit for incoming HTTP requests (as long as the client remains connected), Cron Triggers, Queue consumers, and Durable Object alarms have a 15-minute wall time limit [4]. 3. AbortSignal Support: The AbortSignal is used to cancel pending asynchronous operations [2][5]. When a signal passed to fetch is aborted, the fetch promise rejects with an AbortError [5]. 4. Context: Ensure you handle the fetch operation within your worker's execution context [1][6]. If you need to perform work after the response is sent, use ctx.waitUntil() [4]. For detailed limits, see the official Cloudflare Workers Limits documentation [3].

Citations:


Add deadlines to Birdflop external requests.

dohQuery() and fetchPanelIps() make unbounded Cloudflare fetch() calls, and these paths run during server creation plus detail-page refreshes. Add the same abort/timeout policy to both calls. The stale IP-cache fallback should remain for the panel refresh.

📍 Affects 1 file
  • src/util/serverlist/birdflop.ts#L50-L53 (this comment)
  • src/util/serverlist/birdflop.ts#L168-L176
🤖 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 `@src/util/serverlist/birdflop.ts` around lines 50 - 53, Update both dohQuery()
and fetchPanelIps() to apply the project’s existing abort/timeout policy to
their external fetch calls, ensuring neither request can remain unbounded.
Preserve fetchPanelIps()’s stale IP-cache fallback when the timed request fails.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@src/components/rgbirdflop/presets/PresetPreview.tsx`:
- Around line 48-55: Update getGradient to return undefined when colors is
absent or has length zero, before constructing the linear-gradient string.
Preserve the existing gradient generation for non-empty ColorStop arrays.

In `@src/routes/resources/rgb/presets/index.tsx`:
- Around line 442-445: The default perPage value of 20 is missing from both
pagination controls. Update the Pagination instances at
src/routes/resources/rgb/presets/index.tsx lines 442-445 and 474-477 to include
20 in their perPageOptions lists, keeping both controls consistent.
🪄 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: 1dccde48-8f54-41ef-89ca-3e3d44e3876e

📥 Commits

Reviewing files that changed from the base of the PR and between cf3b153 and bddc927.

📒 Files selected for processing (10)
  • drizzle/migrations/meta/0012_snapshot.json
  • drizzle/migrations/meta/_journal.json
  • src/components/Elements/MotdText.tsx
  • src/components/Elements/Output.tsx
  • src/components/Elements/Pagination.tsx
  • src/components/ServerList/ServerCard.tsx
  • src/components/rgbirdflop/presets/PresetPreview.tsx
  • src/routes/resources/rgb/presets/index.tsx
  • src/routes/serverlist/[slug]/index.tsx
  • src/routes/serverlist/index.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • drizzle/migrations/meta/_journal.json
  • src/components/Elements/Output.tsx
  • drizzle/migrations/meta/0012_snapshot.json
  • src/components/ServerList/ServerCard.tsx
  • src/routes/serverlist/[slug]/index.tsx
  • src/routes/serverlist/index.tsx

Comment on lines +48 to +55
export function getGradient(colors?: ColorStop[]) {
if (!colors) {
return undefined;
}
return `linear-gradient(to bottom right, ${colors
?.map((color) => `${color.hex}10 ${color.pos}%`)
.join(', ')})`;
}

Copy link
Copy Markdown
Contributor

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

Handle empty color arrays.

ColorStop[] permits []. The current code then generates linear-gradient(to bottom right, ), which is invalid CSS. Return undefined when colors.length === 0.

Proposed fix
 export function getGradient(colors?: ColorStop[]) {
-  if (!colors) {
+  if (!colors?.length) {
     return undefined;
   }
📝 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
export function getGradient(colors?: ColorStop[]) {
if (!colors) {
return undefined;
}
return `linear-gradient(to bottom right, ${colors
?.map((color) => `${color.hex}10 ${color.pos}%`)
.join(', ')})`;
}
export function getGradient(colors?: ColorStop[]) {
if (!colors?.length) {
return undefined;
}
return `linear-gradient(to bottom right, ${colors
?.map((color) => `${color.hex}10 ${color.pos}%`)
.join(', ')})`;
}
🤖 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 `@src/components/rgbirdflop/presets/PresetPreview.tsx` around lines 48 - 55,
Update getGradient to return undefined when colors is absent or has length zero,
before constructing the linear-gradient string. Preserve the existing gradient
generation for non-empty ColorStop arrays.

Comment on lines +442 to +445
totalCount={presetCount}
rowsLength={publicPresets.length}
itemLabel="presets"
perPageOptions={[12, 24, 48, 96]}

Copy link
Copy Markdown
Contributor

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:

#!/usr/bin/env bash
set -euo pipefail

ast-grep outline src/components/Elements/Pagination.tsx --items all
rg -n -C 6 'perPageOptions|perPage|<select|option' \
  src/components/Elements/Pagination.tsx \
  src/routes/resources/rgb/presets/index.tsx

Repository: birdflop/web

Length of output: 14266


Keep the default perPage selectable in both pagination controls.

perPage defaults to 20, but both <Pagination> instances pass [12, 24, 48, 96]. When no perPage URL parameter is present, the page renders 20 results while the per-page options do not include the active selection. Include 20 in both perPageOptions lists, or change the loader default to an option that is included.

📍 Affects 1 file
  • src/routes/resources/rgb/presets/index.tsx#L442-L445 (this comment)
  • src/routes/resources/rgb/presets/index.tsx#L474-L477
🤖 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 `@src/routes/resources/rgb/presets/index.tsx` around lines 442 - 445, The
default perPage value of 20 is missing from both pagination controls. Update the
Pagination instances at src/routes/resources/rgb/presets/index.tsx lines 442-445
and 474-477 to include 20 in their perPageOptions lists, keeping both controls
consistent.

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.

3 participants