Auto-refuel on dispatch; remove manual refueling UI/API/action (ISS-082, v0.42.0)
CI / build + unit + e2e (pull_request) Successful in 17m25s
CI / build + unit + e2e (pull_request) Successful in 17m25s
This commit is contained in:
+5
-80
@@ -3,7 +3,7 @@
|
||||
import { useCallback, useEffect, useRef, useState, type ChangeEvent } from 'react';
|
||||
import type { GeoJsonFeatureCollection } from '@/lib/airports';
|
||||
import type { PlaneState, StateSnapshot } from '@/lib/game/state';
|
||||
import { FUEL_DISCOUNT_PER_REP, MAX_FUEL_DISCOUNT, PLANE_TYPES, repRate } from '@/lib/game/catalog';
|
||||
import { PLANE_TYPES } from '@/lib/game/catalog';
|
||||
import { abandonPenalty } from '@/lib/game/penalty';
|
||||
import { formatCash, formatDeadlineRelative, formatFuel, formatSimClock, formatSpecialLabel } from '@/lib/format';
|
||||
import { TIME_SCALES, scaleToSlider, sliderToScale } from '@/lib/timeScale';
|
||||
@@ -19,7 +19,7 @@ interface LeftPaneProps {
|
||||
// The shell's single-poll refresh — called after a sim-control POST so the pane updates immediately.
|
||||
onRefresh: () => void;
|
||||
// ISS-021 (v0.20.0): push the full snapshot returned by an action endpoint into the shared shell
|
||||
// state — needed now that the per-plane cargo rows' refuel/fly/abandon actions live in this pane.
|
||||
// state — needed now that the per-plane cargo rows' fly/abandon actions live in this pane.
|
||||
applySnapshot: (snapshot: StateSnapshot) => void;
|
||||
// ISS-002 (v0.15.0): report fleet-row hovers to the shell so the map can highlight the plane.
|
||||
onHover: (target: HoverTarget | null) => void;
|
||||
@@ -101,11 +101,6 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
|
||||
const [relocatePlaneId, setRelocatePlaneId] = useState<string | null>(null);
|
||||
const [relocateDestIcao, setRelocateDestIcao] = useState<string | null>(null);
|
||||
|
||||
// ISS-035 (v0.23.0): Refuel-to-full selection — moved here from the right-pane market row; same
|
||||
// "effective selection" pattern as Relocate above: the user's pick while it stays refuelable, else
|
||||
// a sensible default (first grounded plane with fuel below max).
|
||||
const [refuelPlaneId, setRefuelPlaneId] = useState<string | null>(null);
|
||||
|
||||
// ISS-010 (v0.18.0): one-time /api/airports fetch per page life — destination options for the
|
||||
// Relocate section, sorted by name so e2e selection is stable. Same promise-ref pattern as Map.tsx;
|
||||
// on failure airportOptions stays null and the Relocate button simply remains disabled. Moved here
|
||||
@@ -202,7 +197,7 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
|
||||
// endpoint (pure repositioning, no package needed since v0.12.0). Moved here from the right-pane
|
||||
// market row; with state null both fall back to null and the section isn't rendered.
|
||||
// ISS-053 (v0.32.0): widened from grounded-only to ALL planes — a flying plane's relocation is
|
||||
// queued instead of started immediately (see relocate-go below). `grounded` stays for Refuel.
|
||||
// queued instead of started immediately (see relocate-go below).
|
||||
const relocatable = state ? state.planes : [];
|
||||
const relocatePlane = relocatable.find((p) => p.id === relocatePlaneId) ?? relocatable[0] ?? null;
|
||||
const relocateDest =
|
||||
@@ -213,22 +208,6 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
|
||||
relocatePlane?.status === 'flying' && relocateDest !== null &&
|
||||
(relocatePlane.destIcao === relocateDest.icao || relocatePlane.queue.includes(relocateDest.icao));
|
||||
|
||||
// ISS-035 (v0.23.0): Refuel-to-full — any GROUNDED plane with fuel below max can be topped up via the
|
||||
// existing refuel endpoint (no body → server defaults to mode 'full'). Moved here from the right-pane
|
||||
// market row; grounded-only, and with state null falls back to null.
|
||||
const grounded = relocatable.filter((p) => p.status === 'available');
|
||||
const refuelable = grounded.filter((p) => p.fuel < p.maxFuel);
|
||||
const refuelPlane =
|
||||
(state?.planes.find((p) => p.id === refuelPlaneId && p.status === 'available' && p.fuel < p.maxFuel)) ??
|
||||
refuelable[0] ??
|
||||
null;
|
||||
// v0.38.0 (ISS-072): rep-scaled preview — same formula as the server's refuelPlane 'full' mode.
|
||||
const fuelDiscountRate = state ? repRate(state.account.reputation, FUEL_DISCOUNT_PER_REP, MAX_FUEL_DISCOUNT) : 0;
|
||||
const refuelCost =
|
||||
refuelPlane && state
|
||||
? Math.round((refuelPlane.maxFuel - refuelPlane.fuel) * state.fuelPricePerL * (1 - fuelDiscountRate) * 100) / 100
|
||||
: null;
|
||||
|
||||
// Slider commit (v0.9.1): the displayed value follows the drag optimistically via
|
||||
// pendingScale; POST /api/sim fires after a 250 ms debounce, or immediately when the
|
||||
// user releases the slider (browsers fire a native 'change' on release). The server's
|
||||
@@ -444,16 +423,8 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
|
||||
Fly to {pkg.destIcao}
|
||||
</span>
|
||||
</button>
|
||||
{/* ISS-082 (v0.42.0): the per-leg Refuel button is gone — dispatch auto-refuels. */}
|
||||
<div className="onboard-actions">
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`refuel-for-${pkg.id}`}
|
||||
className="pane-btn"
|
||||
disabled={actionsBusy || p.status !== 'available'}
|
||||
onClick={() => void runAction(`/api/planes/${p.id}/refuel`, { mode: 'forFlight', packageId: pkg.id })}
|
||||
>
|
||||
Refuel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`abandon-${pkg.id}`}
|
||||
@@ -586,53 +557,7 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ISS-035 (v0.23.0): moved here from the right-pane market row — it belongs with everything else
|
||||
about our planes, next to Relocate. Plain section: no hover handlers (refuel is not a route). */}
|
||||
<section className="pane-section" data-testid="refuel-section">
|
||||
<h3 className="pane-heading">Refuel</h3>
|
||||
{/* Moved verbatim from the right-pane Market section — same testids/labels; the per-package
|
||||
"Refuel" button in the fleet rows (mode 'forFlight') is a different feature and stays there. */}
|
||||
<div className="market-row">
|
||||
{refuelable.length === 0 ? (
|
||||
<span data-testid="market-refuel-none" className="pane-muted">
|
||||
All tanks full.
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<select
|
||||
data-testid="market-refuel-plane"
|
||||
aria-label="Plane to refuel"
|
||||
value={refuelPlane?.id ?? ''}
|
||||
onChange={(e) => setRefuelPlaneId(e.target.value || null)}
|
||||
>
|
||||
{refuelable.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{PLANE_TYPES[p.model]?.name ?? p.model} — {Math.round(p.fuel)} / {p.maxFuel} L
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span data-testid="market-refuel-cost" className="market-preview">
|
||||
{refuelCost !== null ? formatCash(refuelCost) : ''}
|
||||
</span>
|
||||
{/* v0.38.0 (ISS-072): rep discount hint — only when the account actually gets one. */}
|
||||
{fuelDiscountRate > 0 && (
|
||||
<span data-testid="refuel-discount" className="special-badge">−{Math.round(fuelDiscountRate * 100)}%</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
data-testid="market-refuel"
|
||||
className="pane-btn"
|
||||
disabled={actionsBusy || refuelPlane === null}
|
||||
onClick={() => {
|
||||
if (refuelPlane) void runAction(`/api/planes/${refuelPlane.id}/refuel`);
|
||||
}}
|
||||
>
|
||||
Refuel to full
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
{/* ISS-082 (v0.42.0): the "Refuel to full" section is gone — dispatch auto-refuels instead. */}
|
||||
|
||||
<section className="pane-section">
|
||||
<h3 className="pane-heading">Time</h3>
|
||||
|
||||
Reference in New Issue
Block a user