querysub 0.564.0 → 0.566.0

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.
package/appSecrets.ts CHANGED
@@ -24,7 +24,7 @@ export const getCloudflareCreds = lazy(async (): Promise<{ key: string; email: s
24
24
  /** Serves the secret keys sliftutils' getSecret expects, reading them the way querysub already does: backblaze creds off disk (getBackblazePath), cloudflare creds from the keys archives bucket (with its local file fallback). */
25
25
  export async function getAppSecret(key: string): Promise<string | undefined> {
26
26
  // Import query sub so our logs go to our actual log server. Otherwise we won't know if we get any errors.
27
- Querysub;
27
+ //Querysub;
28
28
  if (key.startsWith("backblaze.json.")) {
29
29
  let creds = JSON.parse(fs.readFileSync(getBackblazePath(), "utf8")) as { applicationKeyId?: string; applicationKey?: string };
30
30
  if (key === "backblaze.json.applicationKeyId") return creds.applicationKeyId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.564.0",
3
+ "version": "0.566.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -71,7 +71,7 @@
71
71
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
72
72
  "pako": "^2.1.0",
73
73
  "peggy": "^5.0.6",
74
- "sliftutils": "^1.7.42",
74
+ "sliftutils": "^1.7.43",
75
75
  "socket-function": "^1.2.26",
76
76
  "terser": "^5.31.0",
77
77
  "typenode": "^6.6.1",
@@ -19,6 +19,9 @@ function createSourceWindows(
19
19
  let timeRanges: [number, number][] = [];
20
20
  for (let windowIndex = 0; windowIndex < sourceWindows.length; windowIndex++) {
21
21
  let startTime = sourceWindows[windowIndex].startTime;
22
+ if (windowIndex === 0) {
23
+ startTime = 0;
24
+ }
22
25
  let endTime = sourceWindows[windowIndex + 1]?.startTime ?? Number.MAX_SAFE_INTEGER;
23
26
  if (startTime > endTime) throw new Error(`Source window ${formatDateTime(startTime)} starts after end of ${formatDateTime(endTime)}`);
24
27
  timeRanges.push([startTime, endTime]);
@@ -40,29 +43,29 @@ function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
40
43
  const BACKBLAZE = `https://f002.backblazeb2.com/file/${bucket}/storage/storagerouting.json`;
41
44
  let sources = createSourceWindows(overrides, [
42
45
  {
43
- startTime: 0,
46
+ startTime: +new Date("2026-07-20 18:00:00-04:00"),
44
47
  sources: [
45
- { type: "remote", url: ONTARIO, route: [0, 0.5], },
46
- { type: "remote", url: HETZNER, route: [0.5, 1], },
48
+ { type: "remote", url: ONTARIO, route: [0.5, 1], },
49
+ { type: "remote", url: HETZNER, route: [0, 0.5], },
47
50
  { type: "remote", url: ONTARIO, },
48
51
  { type: "remote", url: HETZNER, },
49
52
  { type: "backblaze", url: BACKBLAZE, },
50
53
  ]
51
54
  },
52
55
  {
53
- startTime: +new Date("2026-07-20 18:00:00-04:00"),
56
+ startTime: +new Date("2026-07-21 02:59:00-04:00"),
54
57
  sources: [
55
- { type: "remote", url: ONTARIO, route: [0.5, 1], },
56
- { type: "remote", url: HETZNER, route: [0, 0.5], },
58
+ { type: "remote", url: ONTARIO, route: [0, 0.5], },
59
+ { type: "remote", url: HETZNER, route: [0.5, 1], },
57
60
  { type: "remote", url: ONTARIO, },
58
61
  { type: "remote", url: HETZNER, },
59
62
  { type: "backblaze", url: BACKBLAZE, },
60
63
  ]
61
- }
64
+ },
62
65
  ]);
63
66
 
64
67
  return createArchives({
65
- version: 14,
68
+ version: 15,
66
69
  sources,
67
70
  });
68
71
  }
@@ -167,6 +167,12 @@ export class ProcessesView extends qreact.Component<{
167
167
  serviceId?: string;
168
168
  }> {
169
169
  render() {
170
+ // Callers hand us one entry per service instance, so a machine running several instances arrives several times. Each machine's processes are listed once, whatever it is running.
171
+ let machines = new Map<string, { machineId: string; applyNodeId: string }>();
172
+ for (let machine of this.props.machines) {
173
+ if (machines.has(machine.machineId)) continue;
174
+ machines.set(machine.machineId, machine);
175
+ }
170
176
  return <div className={css.vbox(14).fillWidth}>
171
177
  <div className={css.hbox(10).alignItems("center")}>
172
178
  <h3 className={css.flexGrow(1)}>Processes</h3>
@@ -176,7 +182,7 @@ export class ProcessesView extends qreact.Component<{
176
182
  Invalidate
177
183
  </Button>
178
184
  </div>
179
- {this.props.machines.map(machine => <MachineProcesses
185
+ {[...machines.values()].map(machine => <MachineProcesses
180
186
  key={machine.machineId}
181
187
  machineId={machine.machineId}
182
188
  applyNodeId={machine.applyNodeId}
@@ -12,7 +12,7 @@ import type { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig } fr
12
12
  import { showModal } from "../../5-diagnostics/Modal";
13
13
  import { FullscreenModal } from "../../5-diagnostics/FullscreenModal";
14
14
  import { Button } from "../../library-components/Button";
15
- import { Tag, TAG_FONT_SIZE, TAG_WARNING_COLOR, NO_FULL_SYNC_TAG } from "./Tag";
15
+ import { Tag, TAG_FONT_SIZE, TAG_WARNING_COLOR, getSourceTags } from "./Tag";
16
16
  import { StorageSynced, type StorageServerBuckets } from "./StoragePage";
17
17
 
18
18
  module.hotreload = true;
@@ -343,21 +343,6 @@ function showConfigModal(bucketName: string, rawConfig: RemoteConfig): void {
343
343
  });
344
344
  }
345
345
 
346
- function getSourceTags(source: Source): { icon: string; text: string }[] {
347
- let tags: { icon: string; text: string }[] = [];
348
- if (source.public) tags.push({ icon: "🌐", text: "public" });
349
- if (source.immutable) tags.push({ icon: "🔒", text: "immutable" });
350
- if (source.noFullSync) tags.push(NO_FULL_SYNC_TAG);
351
- if (source.intermediate) tags.push({ icon: "⏳", text: "intermediate" });
352
- if (source.type === "remote") {
353
- if (source.fast) tags.push({ icon: "⚡", text: "fast" });
354
- if (source.rawDisk) tags.push({ icon: "💽", text: "rawDisk" });
355
- if (source.writeDelay !== undefined) tags.push({ icon: "⏱", text: `writeDelay ${formatTime(source.writeDelay)}` });
356
- }
357
- if (source.readerDiskLimit !== undefined) tags.push({ icon: "📦", text: `readCache ${formatNumber(source.readerDiskLimit)}B` });
358
- return tags;
359
- }
360
-
361
346
  /** Watching a source pins that server's own live view of the bucket to the top of the page, so disagreements between servers are visible directly. */
362
347
  class WatchButton extends qreact.Component<{ source: Source; bucketName: string }> {
363
348
  render() {
@@ -35,10 +35,6 @@ export class ServiceDetailPage extends qreact.Component {
35
35
  expandedErrors: t.lookup({
36
36
  expanded: t.type(false)
37
37
  }),
38
- // Milliseconds; 0 means no scheduled time picked yet (use Deploy Now instead)
39
- switchTime: t.number(0),
40
- // Seconds until the release goes live; 0 means use the default (DEFAULT_OVERLAP_TIME)
41
- releaseDelayOverride: t.number(0),
42
38
  // Seconds the old instances outlive the release; 0 means use the default (DEFAULT_OVERLAP_TIME). The no-overlap deploy buttons are how you deploy with an actual 0.
43
39
  overlapOverride: t.number(0),
44
40
  });
@@ -64,8 +60,6 @@ export class ServiceDetailPage extends qreact.Component {
64
60
  await MachineServiceController(SocketFunction.browserNodeId()).setServiceConfigs.promise([deployConfig]);
65
61
  Querysub.commit(() => {
66
62
  this.state.editorState = stripReleaseState(deployConfig);
67
- this.state.switchTime = 0;
68
- this.state.releaseDelayOverride = 0;
69
63
  this.state.overlapOverride = 0;
70
64
  });
71
65
  } finally {
@@ -93,16 +87,16 @@ export class ServiceDetailPage extends qreact.Component {
93
87
  modal = showModal({
94
88
  content: <div className={css.fixed.pos(0, 0).size("100vw", "100vh").hsla(0, 0, 0, 0.5).display("flex").alignItems("center").justifyContent("center")}>
95
89
  <div className={css.vbox(12).pad2(20).hsl(0, 0, 12).colorhsl(0, 0, 90).bord2(0, 0, 40) + " keepModalsOpen"}>
96
- <div className={css.boldStyle.fontSize(16)}>⚡ Force deploy now?</div>
90
+ <div className={css.boldStyle.fontSize(16)}>🚀 Deploy?</div>
97
91
  <div>{deployConfig.parameters.overlapTime
98
- ? `This config becomes live immediately. The old instances keep running for ${formatTime(deployConfig.parameters.overlapTime)} before shutting down.`
99
- : "This config becomes live immediately. The old instances are shut down right away, with no overlap."}</div>
92
+ ? `This config goes live now. The old instances keep running for ${formatTime(deployConfig.parameters.overlapTime)} before shutting down.`
93
+ : "This config goes live now. The old instances are shut down as soon as the new ones are up, so anything still connected to them is dropped."}</div>
100
94
  <div className={css.hbox(10)}>
101
95
  <Button hue={0} onClick={() => {
102
96
  modal && modal.close();
103
97
  deployConfig.parameters.releaseTime = Date.now();
104
98
  this.deployConfig(deployConfig);
105
- }}>⚡ Force Deploy Now</Button>
99
+ }}>🚀 Deploy</Button>
106
100
  <Button onClick={() => modal && modal.close()}>Cancel</Button>
107
101
  </div>
108
102
  </div>
@@ -642,70 +636,39 @@ export class ServiceDetailPage extends qreact.Component {
642
636
  <span title={`${configCompareKey(config)} !== ${configCompareKey(deployedConfig)}`}>Only the title / notes changed, so this saves immediately</span>
643
637
  </div>;
644
638
  }
645
- let releaseDelaySeconds = this.state.releaseDelayOverride || Math.round(DEFAULT_OVERLAP_TIME / timeInSecond);
646
- let releaseDelay = releaseDelaySeconds * timeInSecond;
647
639
  let overlapSeconds = this.state.overlapOverride || Math.round(DEFAULT_OVERLAP_TIME / timeInSecond);
648
640
  let overlap = overlapSeconds * timeInSecond;
649
- let scheduledTime = this.state.switchTime;
650
- const makeDeployConfig = (releaseTime: number, overlapTime = overlap) => {
641
+ // A deploy always goes live now. The overlap is what keeps the service up: the old instances keep running for it, so connections have somewhere to go while clients move across.
642
+ const makeDeployConfig = (overlapTime = overlap) => {
651
643
  let deployConfig = deepCloneJSON(config);
652
- deployConfig.parameters.releaseTime = releaseTime;
644
+ deployConfig.parameters.releaseTime = Date.now();
653
645
  deployConfig.parameters.overlapTime = overlapTime;
654
646
  return deployConfig;
655
647
  };
656
- // The switch time is inferred from the release delay, unless one is explicitly set
657
- let effectiveSwitchTime = scheduledTime || now + releaseDelay;
658
648
  return <div className={css.vbox(10).pad2(12).fillWidth.bord2(45, 70, 45).hsl(45, 60, 95)}>
659
649
  <div className={css.boldStyle.fontSize(15)}>
660
650
  ✏️ Edited (only in this browser, until deployed)
661
651
  </div>
662
652
  <ParametersDiff base={liveParameters} next={config.parameters} />
663
- <div className={css.hbox(12).wrap}>
653
+ <div className={css.hbox(12).wrap.alignItems("center")}>
664
654
  <button
665
655
  className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(120, 70, 85).fontWeight("bold")}
666
656
  disabled={this.state.isDeploying}
657
+ title="Deploys now; the old instances keep running for the overlap before shutting down"
667
658
  onClick={() => {
668
- this.deployConfig(makeDeployConfig(scheduledTime || Date.now() + releaseDelay));
659
+ this.confirmForceDeployNow(makeDeployConfig());
669
660
  }}>
670
- {this.state.isDeploying && "⏳ Scheduling..." || scheduledDeploy && "🔁 Replace Scheduled Deploy" || "🚀 Schedule Deploy"}
661
+ {this.state.isDeploying && "⏳ Deploying..." || "🚀 Deploy"}
671
662
  </button>
672
663
  <button
673
- className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(35, 70, 85)}
664
+ className={css.pad2(12, 8).button.bord2(0, 90, 30).hsl(0, 85, 65).colorhsl(0, 0, 100).fontWeight("bold")}
674
665
  disabled={this.state.isDeploying}
675
- title="Deploys with the configured release delay, but the old instances shut down immediately at release time (overlap of 0)"
666
+ title="Deploys now AND shuts the old instances down as soon as the new ones are up (overlap of 0)"
676
667
  onClick={() => {
677
- this.deployConfig(makeDeployConfig(scheduledTime || Date.now() + releaseDelay, 0));
668
+ this.confirmForceDeployNow(makeDeployConfig(0));
678
669
  }}>
679
670
  Deploy, No Overlap
680
671
  </button>
681
- <div className={css.opacity(scheduledTime && 1 || 0.5)}>
682
- <InputLabel
683
- label="Switch at"
684
- isDatetime
685
- value={effectiveSwitchTime}
686
- onChangeValue={value => {
687
- this.state.switchTime = Number(value) || 0;
688
- }}
689
- />
690
- </div>
691
- <span>(in {formatTime(effectiveSwitchTime - now)})</span>
692
- {!!scheduledTime && <button
693
- className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 90)}
694
- onClick={() => {
695
- this.state.switchTime = 0;
696
- }}>
697
- ↺ Reset to delay-based time
698
- </button>}
699
- <div className={css.opacity(scheduledTime && 0.5 || 1)}>
700
- <InputLabel
701
- label="Release in (seconds)"
702
- integer
703
- value={releaseDelaySeconds}
704
- onChangeValue={value => {
705
- this.state.releaseDelayOverride = Number(value) || 0;
706
- }}
707
- />
708
- </div>
709
672
  <InputLabel
710
673
  label="Overlap (seconds)"
711
674
  integer
@@ -715,29 +678,6 @@ export class ServiceDetailPage extends qreact.Component {
715
678
  }}
716
679
  />
717
680
  </div>
718
- {!!scheduledTime && scheduledTime <= now && <span className={css.colorhsl(0, 85, 45).fontWeight("bold")}>
719
- ⚠️ The switch time is in the past — deploying switches immediately
720
- </span>}
721
- <div className={css.hbox(8)}>
722
- <button
723
- className={css.pad2(12, 8).button.bord2(45, 80, 35).hsl(45, 85, 82)}
724
- disabled={this.state.isDeploying}
725
- title="Deploys immediately; the old instances keep running for their configured overlap before shutting down"
726
- onClick={() => {
727
- this.confirmForceDeployNow(makeDeployConfig(Date.now()));
728
- }}>
729
- ⚡ Force Deploy Now
730
- </button>
731
- <button
732
- className={css.pad2(12, 8).button.bord2(0, 90, 30).hsl(0, 85, 65).colorhsl(0, 0, 100).fontWeight("bold")}
733
- disabled={this.state.isDeploying}
734
- title="Deploys immediately AND shuts down the old instances immediately (overlap of 0)"
735
- onClick={() => {
736
- this.confirmForceDeployNow(makeDeployConfig(Date.now(), 0));
737
- }}>
738
- ⚡ Force Deploy Now, No Overlap
739
- </button>
740
- </div>
741
681
  </div>;
742
682
  })()}
743
683
 
@@ -17,7 +17,7 @@ import { isDefined } from "../../misc";
17
17
  import { STORAGE_ACCOUNT } from "../../-a-archives/archives2";
18
18
  import { Table } from "../../5-diagnostics/Table";
19
19
  import { RouteConfigView } from "./RouteConfigView";
20
- import { Tag, NO_FULL_SYNC_TAG } from "./Tag";
20
+ import { Tag, SourceTag, getSourceTags } from "./Tag";
21
21
 
22
22
  module.hotreload = true;
23
23
 
@@ -134,7 +134,7 @@ type BucketRow = {
134
134
  state: string;
135
135
  files: string;
136
136
  bytes: string;
137
- noFullSync: boolean;
137
+ flags: SourceTag[];
138
138
  indexSources: ArchivesConfig["indexSources"];
139
139
  readerDiskLimit: string;
140
140
  writes: string;
@@ -152,13 +152,13 @@ const DISK_BAR_TYPE = "DISK";
152
152
  const ACTIVE_STATE = "active";
153
153
  const INACTIVE_STATE = "inactive";
154
154
 
155
- /** Whether this server only caches what is read of the bucket instead of keeping a full copy, read off its own entry in the bucket's routing config. */
156
- function getNoFullSync(serverUrl: string, bucketName: string, remoteConfig: RemoteConfig | undefined): boolean {
155
+ /** The flags this server's own entries in the bucket's routing config set - how it holds the bucket, which is not something the bucket's own state reports. */
156
+ function getBucketFlags(serverUrl: string, bucketName: string, remoteConfig: RemoteConfig | undefined): SourceTag[] {
157
157
  let host: URL;
158
158
  try {
159
159
  host = new URL(serverUrl);
160
160
  } catch {
161
- return false;
161
+ return [];
162
162
  }
163
163
  let selfEntries: HostedConfig[] = [];
164
164
  for (let source of remoteConfig?.sources || []) {
@@ -173,7 +173,15 @@ function getNoFullSync(serverUrl: string, bucketName: string, remoteConfig: Remo
173
173
  // A malformed url in the config is not this column's problem
174
174
  }
175
175
  }
176
- return selfEntries.some(x => x.noFullSync);
176
+ // A server can hold several entries for one bucket (different windows or route shards), so the flags are the union of theirs, deduped by what they say
177
+ let flags = new Map<string, SourceTag>();
178
+ for (let entry of selfEntries) {
179
+ for (let tag of getSourceTags(entry)) {
180
+ if (flags.has(tag.text)) continue;
181
+ flags.set(tag.text, tag);
182
+ }
183
+ }
184
+ return [...flags.values()];
177
185
  }
178
186
 
179
187
  /** How much fast-mode coalescing saved: every accepted write over the ones that actually reached a source. */
@@ -195,7 +203,7 @@ function getBucketRows(servers: StorageServerBuckets[]): BucketRow[] {
195
203
  state: "",
196
204
  files: "",
197
205
  bytes: "",
198
- noFullSync: false,
206
+ flags: [],
199
207
  indexSources: undefined,
200
208
  readerDiskLimit: "",
201
209
  writes: "",
@@ -225,7 +233,7 @@ function getBucketRows(servers: StorageServerBuckets[]): BucketRow[] {
225
233
  state: bucket.active && ACTIVE_STATE || INACTIVE_STATE,
226
234
  files: config?.index && formatNumber(config.index.fileCount) || "",
227
235
  bytes: config?.index && formatNumber(config.index.byteCount) + "B" || "",
228
- noFullSync: getNoFullSync(server.url, bucket.bucketName, config?.remoteConfig),
236
+ flags: getBucketFlags(server.url, bucket.bucketName, config?.remoteConfig),
229
237
  indexSources: config?.indexSources,
230
238
  readerDiskLimit: config?.readerDiskLimit && formatNumber(config.readerDiskLimit) + "B" || "",
231
239
  writes: bucket.writeStats && formatNumber(bucket.writeStats.originalWrites) || "",
@@ -581,10 +589,12 @@ export class StoragePage extends qreact.Component {
581
589
  },
582
590
  files: { title: "Files" },
583
591
  bytes: { title: "Bytes" },
584
- noFullSync: {
585
- title: "Sync",
586
- // Nothing to show when a full copy is kept - that is the default, and the tag is the flag
587
- formatter: noFullSync => noFullSync && <Tag icon={NO_FULL_SYNC_TAG.icon} text={NO_FULL_SYNC_TAG.text} /> || ""
592
+ flags: {
593
+ title: "Flags",
594
+ // Only flags that are set produce a tag, so an empty cell means every default
595
+ formatter: flags => <div className={css.hbox(4).wrap}>
596
+ {(flags || []).map(tag => <Tag key={tag.text} icon={tag.icon} text={tag.text} />)}
597
+ </div>
588
598
  },
589
599
  indexSources: {
590
600
  title: "Index sources",
@@ -1,5 +1,7 @@
1
1
  import { qreact } from "../../4-dom/qreact";
2
2
  import { css } from "typesafecss";
3
+ import { formatNumber, formatTime } from "socket-function/src/formatting/format";
4
+ import type { HostedConfig, BackblazeConfig } from "sliftutils/storage/IArchives";
3
5
 
4
6
  module.hotreload = true;
5
7
 
@@ -9,8 +11,23 @@ export const TAG_COLOR: TagColor = { h: 210, s: 45, l: 88 };
9
11
  export const TAG_WARNING_COLOR: TagColor = { h: 35, s: 90, l: 85 };
10
12
  export const TAG_FONT_SIZE = 11;
11
13
 
12
- /** A source flag, shown wherever that flag matters. The tag is only ever rendered when the flag is on - an absent tag is the default, so there is nothing to show for it. */
13
- export const NO_FULL_SYNC_TAG = { icon: "⇣", text: "noFullSync" };
14
+ export type SourceTag = { icon: string; text: string };
15
+
16
+ /** Every flag a source has set. Only flags that are on produce a tag - an absent tag is the default, so there is nothing to show for it. */
17
+ export function getSourceTags(source: HostedConfig | BackblazeConfig): SourceTag[] {
18
+ let tags: SourceTag[] = [];
19
+ if (source.public) tags.push({ icon: "🌐", text: "public" });
20
+ if (source.immutable) tags.push({ icon: "🔒", text: "immutable" });
21
+ if (source.noFullSync) tags.push({ icon: "⇣", text: "noFullSync" });
22
+ if (source.intermediate) tags.push({ icon: "⏳", text: "intermediate" });
23
+ if (source.type === "remote") {
24
+ if (source.fast) tags.push({ icon: "⚡", text: "fast" });
25
+ if (source.rawDisk) tags.push({ icon: "💽", text: "rawDisk" });
26
+ if (source.writeDelay !== undefined) tags.push({ icon: "⏱", text: `writeDelay ${formatTime(source.writeDelay)}` });
27
+ }
28
+ if (source.readerDiskLimit !== undefined) tags.push({ icon: "📦", text: `readCache ${formatNumber(source.readerDiskLimit)}B` });
29
+ return tags;
30
+ }
14
31
 
15
32
  export class Tag extends qreact.Component<{ icon: string; text: string; warning?: boolean; color?: TagColor; title?: string; }> {
16
33
  render() {
@@ -77,21 +77,16 @@ export class UpdateButtons extends qreact.Component<{
77
77
  state = t.state({
78
78
  isDeploying: t.type(false),
79
79
  });
80
- // Deploys all outdated services to latestRef. "scheduled" delays the release by the configured overlap (an unrelated value, but a reasonable delay); "now" releases immediately but the old instances still run out their configured overlap; "nowNoOverlap" releases immediately AND shuts the old instances down immediately.
81
- private deployAll(outdatedServices: ServiceConfig[], latestRef: string, mode: "scheduled" | "now" | "nowNoOverlap") {
80
+ // Deploys all outdated services to latestRef, always immediately. The overlap is what keeps the service up - the old instances keep running for it, so connections have somewhere to go while clients move across. noOverlap kills them at once instead.
81
+ private deployAll(outdatedServices: ServiceConfig[], latestRef: string, noOverlap: boolean) {
82
82
  this.state.isDeploying = true;
83
83
  // Props are synchronized state, so everything the async work needs is cloned into plain locals HERE, in the synced part
84
84
  let toDeploy = outdatedServices.map(service => {
85
85
  let updated = deepCloneJSON(service);
86
86
  updated.parameters.gitRef = latestRef;
87
- if (mode === "scheduled") {
88
- let overlap = updated.parameters.overlapTime ?? DEFAULT_OVERLAP_TIME;
89
- updated.parameters.releaseTime = Date.now() + overlap;
90
- } else {
91
- updated.parameters.releaseTime = Date.now();
92
- if (mode === "nowNoOverlap") {
93
- updated.parameters.overlapTime = 0;
94
- }
87
+ updated.parameters.releaseTime = Date.now();
88
+ if (noOverlap) {
89
+ updated.parameters.overlapTime = 0;
95
90
  }
96
91
  return updated;
97
92
  });
@@ -112,14 +107,14 @@ export class UpdateButtons extends qreact.Component<{
112
107
  content: <div className={css.vbox(12).pad2(20)}>
113
108
  <div className={css.boldStyle.fontSize(16)}>⚡ Deploy all {outdatedServices.length} services now?</div>
114
109
  <div>{noOverlap
115
- ? "All services release immediately AND their old instances shut down immediately (overlap of 0)."
116
- : "All services release immediately. Their old instances keep running for their configured overlap before shutting down."}</div>
110
+ ? "The old instances are shut down as soon as the new ones are up (overlap of 0), so anything still connected to them is dropped."
111
+ : "The old instances keep running for their configured overlap before shutting down, so connections have somewhere to go while clients move across."}</div>
117
112
  <div className={css.hbox(10)}>
118
113
  <button
119
114
  className={css.pad2(12, 8).button + (noOverlap && css.bord2(0, 90, 30).hsl(0, 85, 65).colorhsl(0, 0, 100).fontWeight("bold") || css.bord2(45, 80, 35).hsl(45, 85, 82))}
120
115
  onClick={() => {
121
116
  modal && modal.close();
122
- this.deployAll(outdatedServices, latestRef, noOverlap ? "nowNoOverlap" : "now");
117
+ this.deployAll(outdatedServices, latestRef, noOverlap);
123
118
  }}>
124
119
  ⚡ Deploy Now
125
120
  </button>
@@ -188,8 +183,9 @@ export class UpdateButtons extends qreact.Component<{
188
183
  <button
189
184
  className={buttonStyle.hsl(120, 70, 90)}
190
185
  disabled={this.state.isDeploying}
186
+ title="Deploys now; the old instances keep running for their configured overlap before shutting down"
191
187
  onClick={() => {
192
- this.deployAll(outdatedServices, gitInfo.latestRef, "scheduled");
188
+ this.confirmDeployAllNow(outdatedServices, gitInfo.latestRef, false);
193
189
  }}
194
190
  >
195
191
  <div>
@@ -204,28 +200,16 @@ export class UpdateButtons extends qreact.Component<{
204
200
  <RenderGitRefInfo gitRef={ref} />
205
201
  </div>)}
206
202
  </button>
207
- <button
208
- className={buttonStyle.hsl(45, 85, 82)}
209
- disabled={this.state.isDeploying}
210
- title="Deploys immediately; the old instances keep running for their configured overlap before shutting down"
211
- onClick={() => {
212
- this.confirmDeployAllNow(outdatedServices, gitInfo.latestRef, false);
213
- }}
214
- >
215
- <div>
216
- {bigEmoji("⚡")} <span>{this.state.isDeploying && "⏳ Deploying..." || `Deploy All (${outdatedServices.length}) Now (no delay)`}</span>
217
- </div>
218
- </button>
219
203
  <button
220
204
  className={buttonStyle.hsl(0, 85, 70)}
221
205
  disabled={this.state.isDeploying}
222
- title="Deploys immediately AND shuts down the old instances immediately (overlap of 0)"
206
+ title="Deploys now AND shuts the old instances down as soon as the new ones are up (overlap of 0)"
223
207
  onClick={() => {
224
208
  this.confirmDeployAllNow(outdatedServices, gitInfo.latestRef, true);
225
209
  }}
226
210
  >
227
211
  <div>
228
- {bigEmoji("⚡")} <span>{this.state.isDeploying && "⏳ Deploying..." || `Deploy All (${outdatedServices.length}) Now (no delay, no overlap)`}</span>
212
+ {bigEmoji("⚡")} <span>{this.state.isDeploying && "⏳ Deploying..." || `Deploy All (${outdatedServices.length}), No Overlap`}</span>
229
213
  </div>
230
214
  </button>
231
215
  </>}