Skip to content

[Fix] Server provider availability#1158

Merged
RichardAnderson merged 8 commits into
vitodeploy:4.xfrom
RichardAnderson:fix/server-provider-availability
Jun 13, 2026
Merged

[Fix] Server provider availability#1158
RichardAnderson merged 8 commits into
vitodeploy:4.xfrom
RichardAnderson:fix/server-provider-availability

Conversation

@RichardAnderson

@RichardAnderson RichardAnderson commented Jun 13, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Monthly pricing shown for available plans; unavailable plans labelled and disabled
    • Available plans prioritised in listings; plan selector normalises mixed plan formats
    • Real-time socket events emitted when server providers are created, updated or deleted
  • Tests

    • Added coverage for plan availability, ordering, label/pricing formatting and provider socket events
  • Documentation

    • API docs updated: plans endpoints now return a map of plan-id → label

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Controllers now return provider()->availablePlans(region) and providers produce structured plan entries ({label, available}). AbstractProvider filters available-only labels for API responses. Frontend normalises mixed plan shapes, disables unavailable options, and socket events are emitted for provider create/edit/delete. Tests and OpenAPI docs updated.

Changes

Server Provider Plans API with Availability

Layer / File(s) Summary
Interface contract and base filtering
app/ServerProviders/ServerProvider.php, app/ServerProviders/AbstractProvider.php
New availablePlans(?string $region): array declared on the interface; AbstractProvider::availablePlans filters plans() to exclude entries with available: false and normalises array-shaped plans to labels.
DigitalOcean & Vultr plan shape changes
app/ServerProviders/DigitalOcean.php, app/ServerProviders/Vultr.php
Both providers compute per-plan available booleans, return slug/id => { label, available }, append formatted monthly price to available plan labels, and sort results with available plans first.
Hetzner & Linode region-aware plans
app/ServerProviders/Hetzner.php, app/ServerProviders/Linode.php
Hetzner computes availability from locations and deprecation rules and extracts region net pricing; Linode computes availability from region capabilities and region-specific pricing. Both return keyed {label, available} maps sorted by availability and add private helpers for availability and price extraction.
Controller endpoint wiring
app/Http/Controllers/API/ServerProviderController.php, app/Http/Controllers/API/UserServerProviderController.php
Both controllers' plans endpoints switched from provider()->plans($region) to provider()->availablePlans($region) for JSON responses.
Frontend plan handling and UI
resources/js/pages/servers/components/create-server.tsx
Add PlanOption type and normalizePlan() to support string or {label, available} plan entries; update plan selector to disable unavailable options and show “(unavailable)” markers; refetch providers on sheet open and socket events.
Socket event dispatches
app/Actions/ServerProvider/CreateServerProvider.php, app/Actions/ServerProvider/EditServerProvider.php, app/Actions/ServerProvider/DeleteServerProvider.php
Emit server-provider.created, server-provider.updated, and server-provider.deleted SocketEvent dispatches after persistence, with SocketEventDTO and ServerProviderResource payloads.
OpenAPI docs
public/api-docs/openapi/server-providers.yaml, public/api-docs/openapi/user-server-providers.yaml
Plans endpoints' 200 responses changed from arrays of plan objects to object maps keyed by plan identifier with string label values; examples updated.
API & feature test coverage
tests/Feature/API/ServerProvidersTest.php, tests/Feature/ServerProvidersTest.php
Add Hetzner API test asserting only available types are returned; feature tests for DigitalOcean, Vultr, Linode, and Hetzner validating availability flags, ordering and label formatting; tests for create/delete dispatching SocketEvent.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ServerProviderController
  participant ServerProvider
  participant AbstractProvider
  Client->>ServerProviderController: GET /plans (region)
  ServerProviderController->>ServerProvider: provider()->availablePlans(region)
  ServerProvider->>AbstractProvider: availablePlans(region)
  AbstractProvider->>ServerProvider: filters plans(), returns flat labels
  ServerProvider-->>ServerProviderController: available plans map
  ServerProviderController-->>Client: 200 JSON (plan-id => label)
Loading

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4
✅ 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 '[Fix] Server provider availability' directly and accurately summarises the main change: introducing availability-aware plan filtering and display across server provider implementations.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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.

Pull request overview

This PR updates server-provider plan handling to surface per-region availability (and pricing labels) in the web UI, while changing the API plans endpoints to return a flat, available-only plan map.

Changes:

  • Provider plans() implementations (Hetzner/DigitalOcean/Vultr/Linode) now compute available per plan+region and format labels with monthly pricing only when available.
  • Frontend server creation UI now supports both legacy string plan labels and { label, available } plan objects, disabling unavailable options.
  • API plans endpoints now return availablePlans() (flat planKey => label map) instead of provider-specific structures.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/Feature/ServerProvidersTest.php Adds feature tests asserting plan availability/label formatting and ordering for multiple providers.
tests/Feature/API/ServerProvidersTest.php Adds an API test asserting Hetzner plans are returned as a flat, available-only map.
resources/js/pages/servers/components/create-server.tsx Normalizes plan data (string vs object) and disables unavailable plans in the selector UI.
app/ServerProviders/AbstractProvider.php Introduces availablePlans() helper to flatten and filter plans for API responses.
app/ServerProviders/ServerProvider.php Extends the provider contract with availablePlans().
app/ServerProviders/Hetzner.php Computes availability using location flags/deprecation and appends price when available.
app/ServerProviders/DigitalOcean.php Returns all sizes with available per region and adds price only for available sizes.
app/ServerProviders/Vultr.php Returns all plans with availability by region and adds price only for available plans.
app/ServerProviders/Linode.php Computes availability by region capabilities and resolves regional pricing where present.
app/Http/Controllers/API/UserServerProviderController.php Switches user-scoped API plans response to availablePlans().
app/Http/Controllers/API/ServerProviderController.php Switches project-scoped (deprecated) API plans response to availablePlans().

Comment thread tests/Feature/API/ServerProvidersTest.php
Comment thread app/Http/Controllers/API/UserServerProviderController.php
Comment thread app/Http/Controllers/API/ServerProviderController.php

@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

Caution

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

⚠️ Outside diff range comments (1)
resources/js/pages/servers/components/create-server.tsx (1)

212-239: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include form in the useEffect dependency array to avoid stale closures.

The useEffect references form.setData in the event handlers but does not include form in the dependency array. This can lead to stale closure bugs if the form reference changes.

🔧 Proposed fix
   useEffect(() => {
     if (defaultOpen) {
       setOpen(defaultOpen);
     }

     const handleRemoveService = (d: unknown) => {
       const service = d as Service;
       form.setData((data) => ({
         ...data,
         services: data.services.filter((s) => s.type !== service.type || s.name !== service.name || s.version !== service.version),
       }));
     };
     EventBus.on('remove-service', handleRemoveService);

     const handleAddService = (d: unknown) => {
       const service = d as Service;
       form.setData((data) => ({
         ...data,
         services: [...data.services, service],
       }));
     };
     EventBus.on('add-service', handleAddService);

     return () => {
       EventBus.off('remove-service', handleRemoveService);
       EventBus.off('add-service', handleAddService);
     };
-  }, [defaultOpen]);
+  }, [defaultOpen, form]);
🤖 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 `@resources/js/pages/servers/components/create-server.tsx` around lines 212 -
239, The effect registers EventBus handlers that call form.setData
(handleRemoveService and handleAddService) but only lists defaultOpen in the
dependency array, which risks stale closures; update the useEffect dependencies
to include form so the handlers close over the current form instance (i.e., add
form to the dependency array of the useEffect that defines
handleRemoveService/handleAddService and registers them with EventBus) so the
cleanup/off calls also remove the correct handlers.

Source: Coding guidelines

🤖 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/ServerProviders/DigitalOcean.php`:
- Around line 86-88: The PHPDoc for the DigitalOcean sizes response and the
mapping in plans() should treat the size['available'] field as a required
boolean (not optional); update the PHPDoc shape to declare "available: bool" and
in the ->map(function (array $size) use ($region): array { ... }) closure stop
using the fallback ($size['available'] ?? false) — either access
$size['available'] directly or cast it (bool) $size['available'] so the code and
types align with the API contract.

In `@app/ServerProviders/Linode.php`:
- Around line 127-138: The current planIsAvailable method silently treats empty
region capabilities as "available" (fail-open); change it to fail-closed by
returning false when $capabilities === [] and add a warning log when this
fallback happens so monitoring can detect capability-fetch failures; update the
planIsAvailable(string $class, array $capabilities) function to: if
$capabilities === [] then call the class's logger (e.g. $this->logger->warning
or the project's standard logger) with a clear message about missing/failed
region capabilities and return false, otherwise keep the existing match-based
checks for 'gpu', 'premium', and default.

---

Outside diff comments:
In `@resources/js/pages/servers/components/create-server.tsx`:
- Around line 212-239: The effect registers EventBus handlers that call
form.setData (handleRemoveService and handleAddService) but only lists
defaultOpen in the dependency array, which risks stale closures; update the
useEffect dependencies to include form so the handlers close over the current
form instance (i.e., add form to the dependency array of the useEffect that
defines handleRemoveService/handleAddService and registers them with EventBus)
so the cleanup/off calls also remove the correct handlers.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 77680207-f698-46d3-868f-27a031c7af44

📥 Commits

Reviewing files that changed from the base of the PR and between 51d5a26 and 567f992.

📒 Files selected for processing (11)
  • app/Http/Controllers/API/ServerProviderController.php
  • app/Http/Controllers/API/UserServerProviderController.php
  • app/ServerProviders/AbstractProvider.php
  • app/ServerProviders/DigitalOcean.php
  • app/ServerProviders/Hetzner.php
  • app/ServerProviders/Linode.php
  • app/ServerProviders/ServerProvider.php
  • app/ServerProviders/Vultr.php
  • resources/js/pages/servers/components/create-server.tsx
  • tests/Feature/API/ServerProvidersTest.php
  • tests/Feature/ServerProvidersTest.php

Comment thread app/ServerProviders/DigitalOcean.php
Comment thread app/ServerProviders/Linode.php

@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.

Caution

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

⚠️ Outside diff range comments (1)
resources/js/pages/servers/components/create-server.tsx (1)

313-328: ⚠️ Potential issue | 🟡 Minor

Stabilise fetchServerProviders for hook dependency correctness.

fetchServerProviders is recreated each render, but the useEffect that refreshes providers when open is true omits it from the dependency array. Memoise it with useCallback and include it in the useEffect deps (socket handling is safe here because useSocketListener keeps the latest callback via a ref).

Proposed fix
-import React, { FormEventHandler, useEffect, useState } from 'react';
+import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';

-  const fetchServerProviders = async () => {
+  const fetchServerProviders = useCallback(async () => {
     const serverProviders = await axios.get(route('server-providers.json'));
     setServerProviders(serverProviders.data);
-  };
+  }, []);

   useEffect(() => {
     if (open) {
-      fetchServerProviders();
+      void fetchServerProviders();
     }
-  }, [open]);
+  }, [open, fetchServerProviders]);
🤖 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 `@resources/js/pages/servers/components/create-server.tsx` around lines 313 -
328, fetchServerProviders is re-created on every render but is referenced by the
useEffect that runs when open changes; memoise it with React.useCallback (e.g.
const fetchServerProviders = useCallback(async () => { const res = await
axios.get(route('server-providers.json')); setServerProviders(res.data); }, []))
and then include fetchServerProviders in the useEffect dependency array
(useEffect(() => { if (open) fetchServerProviders(); }, [open,
fetchServerProviders])); keep the existing useSocketListener usage as-is since
it safely captures the latest callback via a ref.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@resources/js/pages/servers/components/create-server.tsx`:
- Around line 313-328: fetchServerProviders is re-created on every render but is
referenced by the useEffect that runs when open changes; memoise it with
React.useCallback (e.g. const fetchServerProviders = useCallback(async () => {
const res = await axios.get(route('server-providers.json'));
setServerProviders(res.data); }, [])) and then include fetchServerProviders in
the useEffect dependency array (useEffect(() => { if (open)
fetchServerProviders(); }, [open, fetchServerProviders])); keep the existing
useSocketListener usage as-is since it safely captures the latest callback via a
ref.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 45719168-b451-4312-b8bc-6f35c531f400

📥 Commits

Reviewing files that changed from the base of the PR and between 567f992 and eae3043.

📒 Files selected for processing (8)
  • app/Actions/ServerProvider/CreateServerProvider.php
  • app/Actions/ServerProvider/DeleteServerProvider.php
  • app/Actions/ServerProvider/EditServerProvider.php
  • app/ServerProviders/DigitalOcean.php
  • public/api-docs/openapi/server-providers.yaml
  • public/api-docs/openapi/user-server-providers.yaml
  • resources/js/pages/servers/components/create-server.tsx
  • tests/Feature/ServerProvidersTest.php

@RichardAnderson RichardAnderson merged commit b7c21bc into vitodeploy:4.x Jun 13, 2026
4 checks passed
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