create-planet 1.0.2 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ import { CryptoCore } from "../../../lib/crypto";
6
6
  import { PlanetIdentity } from "../../../lib/identity";
7
7
  import { ConsensusEngine, type TravelPlan } from "../../../lib/consensus";
8
8
  import { WARP_LINKS, PLANET_NAME } from "../../../lib/config";
9
+ import { doQuery, doExec } from "../../../lib/do-storage";
9
10
  import { env } from "cloudflare:workers";
10
11
 
11
12
  const InitiateSchema = z.object({
@@ -24,6 +25,7 @@ const TravelPlanSchema = z.object({
24
25
  status: z.enum(["PREPARING", "PLAN_ACCEPTED"]),
25
26
  traffic_controllers: z.array(z.string()),
26
27
  signatures: z.record(z.string()),
28
+ origin_lists_dest: z.boolean().optional(),
27
29
  });
28
30
 
29
31
  // Returns ms per Flight-Year. Default: 1 hour (production). Override with WARP_MS_PER_FY for dev.
@@ -92,7 +94,7 @@ async function broadcastEvent(
92
94
  }
93
95
  }
94
96
  export const GET: APIRoute = async ({ request }) => {
95
- const { DB } = env as any;
97
+ const { TRAFFIC_CONTROL } = env as any;
96
98
  const url = new URL(request.url);
97
99
  const action = url.searchParams.get("action");
98
100
 
@@ -103,18 +105,18 @@ export const GET: APIRoute = async ({ request }) => {
103
105
  status: 400,
104
106
  });
105
107
  }
106
- const manifest = await discoverSpacePort(targetUrl, DB);
108
+ const manifest = await discoverSpacePort(targetUrl, TRAFFIC_CONTROL);
107
109
  return new Response(JSON.stringify({ has_space_port: manifest !== null }), {
108
110
  status: 200,
109
111
  });
110
112
  }
111
113
 
112
114
  if (action === "neighbors") {
113
- if (!DB || WARP_LINKS.length === 0) {
115
+ if (!TRAFFIC_CONTROL || WARP_LINKS.length === 0) {
114
116
  return new Response(JSON.stringify({ neighbors: [] }), { status: 200 });
115
117
  }
116
118
  const results = await Promise.all(
117
- WARP_LINKS.map((l) => discoverSpacePort(l.url, DB)),
119
+ WARP_LINKS.map((l) => discoverSpacePort(l.url, TRAFFIC_CONTROL)),
118
120
  );
119
121
  const neighbors = results.filter((n): n is PlanetManifest => n !== null);
120
122
  return new Response(JSON.stringify({ neighbors }), { status: 200 });
@@ -126,7 +128,7 @@ export const GET: APIRoute = async ({ request }) => {
126
128
  };
127
129
 
128
130
  export const POST: APIRoute = async ({ request }) => {
129
- const { KV, DB, TRAFFIC_CONTROL } = env as any;
131
+ const { TRAFFIC_CONTROL } = env as any;
130
132
  const url = new URL(request.url);
131
133
  const action = url.searchParams.get("action");
132
134
  const senderOrigin =
@@ -150,37 +152,13 @@ export const POST: APIRoute = async ({ request }) => {
150
152
 
151
153
  switch (action) {
152
154
  case "initiate":
153
- return await handleInitiate(
154
- request,
155
- KV,
156
- DB,
157
- localPlanet,
158
- TRAFFIC_CONTROL,
159
- );
155
+ return await handleInitiate(request, TRAFFIC_CONTROL, localPlanet);
160
156
  case "prepare":
161
- return await handlePrepare(
162
- request,
163
- KV,
164
- DB,
165
- localPlanet,
166
- TRAFFIC_CONTROL,
167
- );
157
+ return await handlePrepare(request, TRAFFIC_CONTROL, localPlanet);
168
158
  case "register":
169
- return await handleRegister(
170
- request,
171
- KV,
172
- DB,
173
- localPlanet,
174
- TRAFFIC_CONTROL,
175
- );
159
+ return await handleRegister(request, TRAFFIC_CONTROL, localPlanet);
176
160
  case "commit":
177
- return await handleCommit(
178
- request,
179
- KV,
180
- DB,
181
- localPlanet,
182
- TRAFFIC_CONTROL,
183
- );
161
+ return await handleCommit(request, TRAFFIC_CONTROL, localPlanet);
184
162
  default:
185
163
  return new Response(JSON.stringify({ error: "Invalid action" }), {
186
164
  status: 400,
@@ -200,24 +178,21 @@ export const POST: APIRoute = async ({ request }) => {
200
178
 
201
179
  async function discoverSpacePort(
202
180
  landingSiteUrl: string,
203
- DB: D1Database,
181
+ TRAFFIC_CONTROL: DurableObjectNamespace,
204
182
  ): Promise<PlanetManifest | null> {
205
183
  try {
206
- const cached: any = await DB.prepare(
207
- `
208
- SELECT * FROM traffic_controllers
209
- WHERE planet_url = ?
210
- AND last_manifest_fetch > ?
211
- `,
212
- )
213
- .bind(landingSiteUrl, Date.now() - 3600000)
214
- .first();
184
+ const cached = await doQuery(
185
+ TRAFFIC_CONTROL,
186
+ `SELECT * FROM traffic_controllers WHERE planet_url = ? AND last_manifest_fetch > ?`,
187
+ [landingSiteUrl, Date.now() - 3600000],
188
+ );
215
189
 
216
- if (cached) {
190
+ if (cached.length > 0) {
191
+ const row: any = cached[0];
217
192
  return {
218
- name: cached.name,
219
- landing_site: cached.planet_url,
220
- space_port: cached.space_port_url,
193
+ name: row.name,
194
+ landing_site: row.planet_url,
195
+ space_port: row.space_port_url,
221
196
  };
222
197
  }
223
198
 
@@ -251,14 +226,11 @@ async function discoverSpacePort(
251
226
  space_port: remoteManifest.space_port,
252
227
  };
253
228
 
254
- await DB.prepare(
255
- `
256
- INSERT OR REPLACE INTO traffic_controllers (planet_url, name, space_port_url, last_manifest_fetch)
257
- VALUES (?, ?, ?, ?)
258
- `,
259
- )
260
- .bind(planet.landing_site, planet.name, planet.space_port, Date.now())
261
- .run();
229
+ await doExec(
230
+ TRAFFIC_CONTROL,
231
+ `INSERT OR REPLACE INTO traffic_controllers (planet_url, name, space_port_url, last_manifest_fetch) VALUES (?, ?, ?, ?)`,
232
+ [planet.landing_site, planet.name, planet.space_port, Date.now()],
233
+ );
262
234
 
263
235
  return planet;
264
236
  } catch (e) {
@@ -269,10 +241,8 @@ async function discoverSpacePort(
269
241
 
270
242
  async function handleInitiate(
271
243
  request: Request,
272
- KV: KVNamespace,
273
- DB: D1Database,
244
+ TRAFFIC_CONTROL: DurableObjectNamespace,
274
245
  localPlanet: any,
275
- TRAFFIC_CONTROL: any,
276
246
  ) {
277
247
  const body = await request.json();
278
248
  const data = InitiateSchema.parse(body);
@@ -299,8 +269,8 @@ async function handleInitiate(
299
269
  const endTimestamp = startTimestamp + travelTimeHours * msPerFY();
300
270
 
301
271
  const discoveryPromises = [
302
- discoverSpacePort(data.destination_url, DB),
303
- ...WARP_LINKS.map((l) => discoverSpacePort(l.url, DB)),
272
+ discoverSpacePort(data.destination_url, TRAFFIC_CONTROL),
273
+ ...WARP_LINKS.map((l) => discoverSpacePort(l.url, TRAFFIC_CONTROL)),
304
274
  ];
305
275
  const [destManifest, ...neighborResults] =
306
276
  await Promise.all(discoveryPromises);
@@ -341,6 +311,64 @@ async function handleInitiate(
341
311
  );
342
312
  }
343
313
 
314
+ // Enforce planet-funded shuttle limits based on neighbor relationship
315
+ const originListsDest = originNeighbors.some(
316
+ (n) => n.landing_site === destManifest.landing_site,
317
+ );
318
+ const destListsOrigin = destNeighbors.some(
319
+ (n) => n.landing_site === localPlanet.landing_site,
320
+ );
321
+
322
+ const shuttleLimit =
323
+ originListsDest && destListsOrigin
324
+ ? 2
325
+ : originListsDest || destListsOrigin
326
+ ? 1
327
+ : 0;
328
+
329
+ const activeRows = await doQuery(
330
+ TRAFFIC_CONTROL,
331
+ `SELECT COUNT(*) as count FROM travel_plans
332
+ WHERE ((origin_url = ? AND destination_url = ?)
333
+ OR (origin_url = ? AND destination_url = ?))
334
+ AND end_timestamp > ?`,
335
+ [
336
+ localPlanet.landing_site,
337
+ destManifest.landing_site,
338
+ destManifest.landing_site,
339
+ localPlanet.landing_site,
340
+ Date.now(),
341
+ ],
342
+ );
343
+
344
+ const activeShuttles = (activeRows[0] as any)?.count ?? 0;
345
+
346
+ console.log(
347
+ `[${localPlanet.name}] Shuttle limit check: ${activeShuttles}/${shuttleLimit} active (${localPlanet.landing_site} ↔ ${destManifest.landing_site})`,
348
+ );
349
+
350
+ const bypassAllowed = (env as any).ALLOW_TEST_SHUTTLE_BYPASS === "true";
351
+ const bypassRequested =
352
+ request.headers.get("X-Bypass-Shuttle-Limit") === "true";
353
+
354
+ if (activeShuttles >= shuttleLimit && !(bypassAllowed && bypassRequested)) {
355
+ const relationship =
356
+ originListsDest && destListsOrigin
357
+ ? "mutual_neighbors"
358
+ : originListsDest || destListsOrigin
359
+ ? "one_sided_neighbors"
360
+ : "non_neighbors";
361
+ return new Response(
362
+ JSON.stringify({
363
+ error: "shuttle_limit_exceeded",
364
+ active_shuttles: activeShuttles,
365
+ limit: shuttleLimit,
366
+ relationship,
367
+ }),
368
+ { status: 422, headers: { "Content-Type": "application/json" } },
369
+ );
370
+ }
371
+
344
372
  // Origin and destination are mandatory TC participants — their votes are always required
345
373
  const mandatoryTCs: PlanetManifest[] = [
346
374
  {
@@ -421,13 +449,14 @@ async function handleInitiate(
421
449
  status: "PREPARING",
422
450
  traffic_controllers: electedTCs.map((tc) => tc.landing_site),
423
451
  signatures: {},
452
+ origin_lists_dest: originListsDest,
424
453
  };
425
454
 
426
- const { privateKey } = await PlanetIdentity.getIdentity(KV);
455
+ const { privateKey } = await PlanetIdentity.getIdentity(TRAFFIC_CONTROL);
427
456
  const signature = await CryptoCore.sign(JSON.stringify(plan), privateKey);
428
457
  plan.signatures[localPlanet.landing_site] = signature;
429
458
 
430
- await ConsensusEngine.savePlanState(KV, plan);
459
+ await ConsensusEngine.savePlanState(TRAFFIC_CONTROL, plan);
431
460
  await ConsensusEngine.broadcast(plan, "prepare", electedTCs);
432
461
 
433
462
  return new Response(JSON.stringify({ plan }), { status: 200 });
@@ -435,10 +464,8 @@ async function handleInitiate(
435
464
 
436
465
  async function handlePrepare(
437
466
  request: Request,
438
- KV: KVNamespace,
439
- DB: D1Database,
467
+ TRAFFIC_CONTROL: DurableObjectNamespace,
440
468
  localPlanet: any,
441
- TRAFFIC_CONTROL: any,
442
469
  ) {
443
470
  const plan = TravelPlanSchema.parse(await request.json());
444
471
 
@@ -464,14 +491,14 @@ async function handlePrepare(
464
491
  throw new Error("Invalid travel time calculation.");
465
492
  }
466
493
 
467
- const { privateKey } = await PlanetIdentity.getIdentity(KV);
494
+ const { privateKey } = await PlanetIdentity.getIdentity(TRAFFIC_CONTROL);
468
495
  const signature = await CryptoCore.sign(JSON.stringify(plan), privateKey);
469
496
  plan.signatures[localPlanet.landing_site] = signature;
470
497
 
471
- await ConsensusEngine.savePlanState(KV, plan);
498
+ await ConsensusEngine.savePlanState(TRAFFIC_CONTROL, plan);
472
499
 
473
500
  const controllersPromises = plan.traffic_controllers.map((url) =>
474
- discoverSpacePort(url, DB),
501
+ discoverSpacePort(url, TRAFFIC_CONTROL),
475
502
  );
476
503
  const controllers = (await Promise.all(controllersPromises)).filter(
477
504
  (n): n is PlanetManifest => n !== null,
@@ -483,10 +510,8 @@ async function handlePrepare(
483
510
  }
484
511
  async function handleRegister(
485
512
  request: Request,
486
- KV: KVNamespace,
487
- DB: D1Database,
513
+ TRAFFIC_CONTROL: DurableObjectNamespace,
488
514
  localPlanet: any,
489
- TRAFFIC_CONTROL: any,
490
515
  ) {
491
516
  const plan = TravelPlanSchema.parse(await request.json());
492
517
 
@@ -532,18 +557,66 @@ async function handleRegister(
532
557
  });
533
558
  }
534
559
 
535
- const alreadyStored = await DB.prepare(
560
+ const alreadyStored = await doQuery(
561
+ TRAFFIC_CONTROL,
536
562
  `SELECT id FROM travel_plans WHERE id = ?`,
537
- )
538
- .bind(plan.id)
539
- .first();
563
+ [plan.id],
564
+ );
540
565
 
541
- if (!alreadyStored) {
542
- await DB.prepare(
566
+ if (alreadyStored.length === 0) {
567
+ // Enforce shuttle limit from destination's perspective.
568
+ // originListsDest is carried in the plan (set at initiation) to avoid a
569
+ // circular HTTP call back to the origin while it is processing handleCommit.
570
+ const destListsOrigin = WARP_LINKS.some(
571
+ (l) => new URL(l.url).origin === new URL(plan.origin_url).origin,
572
+ );
573
+ const originListsDest = plan.origin_lists_dest ?? false;
574
+
575
+ const destShuttleLimit =
576
+ originListsDest && destListsOrigin
577
+ ? 2
578
+ : originListsDest || destListsOrigin
579
+ ? 1
580
+ : 0;
581
+
582
+ const destActiveRows = await doQuery(
583
+ TRAFFIC_CONTROL,
584
+ `SELECT COUNT(*) as count FROM travel_plans
585
+ WHERE ((origin_url = ? AND destination_url = ?)
586
+ OR (origin_url = ? AND destination_url = ?))
587
+ AND end_timestamp > ?`,
588
+ [
589
+ plan.origin_url,
590
+ localPlanet.landing_site,
591
+ localPlanet.landing_site,
592
+ plan.origin_url,
593
+ Date.now(),
594
+ ],
595
+ );
596
+
597
+ if (((destActiveRows[0] as any)?.count ?? 0) >= destShuttleLimit) {
598
+ const relationship =
599
+ originListsDest && destListsOrigin
600
+ ? "mutual_neighbors"
601
+ : originListsDest || destListsOrigin
602
+ ? "one_sided_neighbors"
603
+ : "non_neighbors";
604
+ return new Response(
605
+ JSON.stringify({
606
+ error: "shuttle_limit_exceeded",
607
+ active_shuttles: (destActiveRows[0] as any)?.count ?? 0,
608
+ limit: destShuttleLimit,
609
+ relationship,
610
+ }),
611
+ { status: 422, headers: { "Content-Type": "application/json" } },
612
+ );
613
+ }
614
+
615
+ await doExec(
616
+ TRAFFIC_CONTROL,
543
617
  `INSERT OR IGNORE INTO travel_plans (id, ship_id, origin_url, destination_url, start_timestamp, end_timestamp, status, signatures)
544
618
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
545
- )
546
- .bind(
619
+ [
547
620
  plan.id,
548
621
  plan.ship_id,
549
622
  plan.origin_url,
@@ -552,8 +625,8 @@ async function handleRegister(
552
625
  plan.end_timestamp,
553
626
  plan.status,
554
627
  JSON.stringify(plan.signatures),
555
- )
556
- .run();
628
+ ],
629
+ );
557
630
 
558
631
  const fmt = (n: number) => n.toFixed(1);
559
632
  const originCoordsFormatted = `${fmt(originCoords.x)}:${fmt(originCoords.y)}:${fmt(originCoords.z)}`;
@@ -572,42 +645,41 @@ async function handleRegister(
572
645
 
573
646
  async function handleCommit(
574
647
  request: Request,
575
- KV: KVNamespace,
576
- DB: D1Database,
648
+ TRAFFIC_CONTROL: DurableObjectNamespace,
577
649
  localPlanet: any,
578
- TRAFFIC_CONTROL: any,
579
650
  ) {
580
651
  const incomingPlan = TravelPlanSchema.parse(await request.json());
581
652
  const existing =
582
- (await ConsensusEngine.getPlanState(KV, incomingPlan.id)) || incomingPlan;
653
+ (await ConsensusEngine.getPlanState(TRAFFIC_CONTROL, incomingPlan.id)) ||
654
+ incomingPlan;
583
655
 
584
656
  console.log(
585
657
  `[${localPlanet.name}] Committing travel plan ${incomingPlan.id} (Existing signatures: ${Object.keys(existing.signatures).length}, New signatures: ${Object.keys(incomingPlan.signatures).length})`,
586
658
  );
587
659
 
588
660
  existing.signatures = { ...existing.signatures, ...incomingPlan.signatures };
589
- await ConsensusEngine.savePlanState(KV, existing);
661
+ await ConsensusEngine.savePlanState(TRAFFIC_CONTROL, existing);
590
662
 
591
663
  if (ConsensusEngine.hasQuorum(existing) && existing.status === "PREPARING") {
592
664
  // Check if we already archived it to prevent race condition across TCs
593
- const alreadyArchived = await DB.prepare(
665
+ const alreadyArchived = await doQuery(
666
+ TRAFFIC_CONTROL,
594
667
  `SELECT id FROM travel_plans WHERE id = ?`,
595
- )
596
- .bind(existing.id)
597
- .first();
668
+ [existing.id],
669
+ );
598
670
 
599
- if (!alreadyArchived) {
671
+ if (alreadyArchived.length === 0) {
600
672
  console.log(
601
673
  `[${localPlanet.name}] Quorum reached for plan ${existing.id}. Registering with destination.`,
602
674
  );
603
675
 
604
676
  existing.status = "PLAN_ACCEPTED";
605
- await ConsensusEngine.savePlanState(KV, existing);
677
+ await ConsensusEngine.savePlanState(TRAFFIC_CONTROL, existing);
606
678
 
607
679
  // Register with destination synchronously — must succeed before committing locally
608
680
  const destManifest = await discoverSpacePort(
609
681
  existing.destination_url,
610
- DB,
682
+ TRAFFIC_CONTROL,
611
683
  );
612
684
  if (!destManifest?.space_port) {
613
685
  return new Response(
@@ -642,13 +714,11 @@ async function handleCommit(
642
714
  );
643
715
  }
644
716
 
645
- await DB.prepare(
646
- `
647
- INSERT OR IGNORE INTO travel_plans (id, ship_id, origin_url, destination_url, start_timestamp, end_timestamp, status, signatures)
648
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
649
- `,
650
- )
651
- .bind(
717
+ await doExec(
718
+ TRAFFIC_CONTROL,
719
+ `INSERT OR IGNORE INTO travel_plans (id, ship_id, origin_url, destination_url, start_timestamp, end_timestamp, status, signatures)
720
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
721
+ [
652
722
  existing.id,
653
723
  existing.ship_id,
654
724
  existing.origin_url,
@@ -657,8 +727,8 @@ async function handleCommit(
657
727
  existing.end_timestamp,
658
728
  existing.status,
659
729
  JSON.stringify(existing.signatures),
660
- )
661
- .run();
730
+ ],
731
+ );
662
732
 
663
733
  await broadcastEvent(TRAFFIC_CONTROL, {
664
734
  type: "QUORUM_REACHED",
@@ -2,6 +2,7 @@
2
2
  import md5 from "md5";
3
3
  import { env } from "cloudflare:workers";
4
4
  import { WARP_LINKS, PLANET_NAME, PLANET_DESCRIPTION } from "../lib/config";
5
+ import { doQuery } from "../lib/do-storage";
5
6
 
6
7
  const formatCoord = (n: number) => n.toFixed(2).padStart(6, "0");
7
8
 
@@ -59,38 +60,34 @@ const myCoords = calculateCoordinates(landingSite);
59
60
  const departureBuffer =
60
61
  parseInt(getSimVar("DEPARTURE_BUFFER_MS") || "") || 30 * 1000;
61
62
 
62
- // Cloudflare D1 Integration
63
- const { DB } = (env as any) || {};
63
+ // Durable Object Storage
64
+ const { TRAFFIC_CONTROL } = (env as any) || {};
64
65
 
65
66
  let traffic = [];
66
67
  let archive = [];
67
68
 
68
- if (DB) {
69
+ if (TRAFFIC_CONTROL) {
69
70
  try {
70
71
  const now = Date.now();
71
72
 
72
- const { results: activePlans } = await DB.prepare(
73
- `
74
- SELECT * FROM travel_plans
75
- WHERE end_timestamp > ? AND (origin_url = ? OR destination_url = ?)
76
- ORDER BY start_timestamp ASC
77
- `,
78
- )
79
- .bind(now, landingSite, landingSite)
80
- .all();
73
+ const activePlans = await doQuery(
74
+ TRAFFIC_CONTROL,
75
+ `SELECT * FROM travel_plans
76
+ WHERE end_timestamp > ? AND (origin_url = ? OR destination_url = ?)
77
+ ORDER BY start_timestamp ASC`,
78
+ [now, landingSite, landingSite],
79
+ );
81
80
 
82
81
  // Build a URL→name map from warp links and the traffic_controllers cache
83
82
  const warpNameMap = new Map(WARP_LINKS.map((l) => [l.url, l.name]));
84
83
 
85
- const { results: archiveRecords } = await DB.prepare(
86
- `
87
- SELECT * FROM travel_plans
88
- WHERE end_timestamp <= ? AND (origin_url = ? OR destination_url = ?)
89
- ORDER BY end_timestamp DESC LIMIT 20
90
- `,
91
- )
92
- .bind(now, landingSite, landingSite)
93
- .all();
84
+ const archiveRecords = await doQuery(
85
+ TRAFFIC_CONTROL,
86
+ `SELECT * FROM travel_plans
87
+ WHERE end_timestamp <= ? AND (origin_url = ? OR destination_url = ?)
88
+ ORDER BY end_timestamp DESC LIMIT 20`,
89
+ [now, landingSite, landingSite],
90
+ );
94
91
 
95
92
  const allPlanUrls = [
96
93
  ...(activePlans || []).flatMap((p: any) => [
@@ -107,11 +104,11 @@ if (DB) {
107
104
  );
108
105
  if (uniqueUrls.length > 0) {
109
106
  const placeholders = uniqueUrls.map(() => "?").join(",");
110
- const { results: cachedNames } = await DB.prepare(
107
+ const cachedNames = await doQuery(
108
+ TRAFFIC_CONTROL,
111
109
  `SELECT planet_url, name FROM traffic_controllers WHERE planet_url IN (${placeholders})`,
112
- )
113
- .bind(...uniqueUrls)
114
- .all();
110
+ uniqueUrls,
111
+ );
115
112
  for (const row of cachedNames || [])
116
113
  warpNameMap.set((row as any).planet_url, (row as any).name);
117
114
  }
@@ -557,6 +554,8 @@ const enrichedLinks = WARP_LINKS.map((link, index) => {
557
554
 
558
555
  const closeDeck = () => {
559
556
  flightDeck.classList.add("hidden");
557
+ initiateBtn.disabled = false;
558
+ initiateBtn.textContent = "JUMP";
560
559
  window.dispatchEvent(new CustomEvent("clearSelection"));
561
560
  };
562
561
 
@@ -856,14 +855,12 @@ const enrichedLinks = WARP_LINKS.map((link, index) => {
856
855
  const data = await response.json();
857
856
  if (data.plan) {
858
857
  addTrafficRow(data.plan, filedAt);
859
- flightDeck.classList.add("hidden");
860
- window.dispatchEvent(new CustomEvent("clearSelection"));
858
+ closeDeck();
861
859
  } else if (
862
860
  data.error === "insufficient_controllers" ||
863
861
  data.error === "no_destination_space_port"
864
862
  ) {
865
- flightDeck.classList.add("hidden");
866
- window.dispatchEvent(new CustomEvent("clearSelection"));
863
+ closeDeck();
867
864
  noTravelReason.textContent =
868
865
  NO_TRAVEL_MESSAGES[data.error] || "Travel is not available.";
869
866
  noTravelDialog.classList.remove("hidden");