querysub 0.357.0 → 0.359.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.
Files changed (25) hide show
  1. package/.cursorrules +1 -0
  2. package/package.json +2 -1
  3. package/src/-a-archives/archivesDisk.ts +24 -6
  4. package/src/-a-archives/archivesMemoryCache.ts +41 -17
  5. package/src/deployManager/components/MachineDetailPage.tsx +45 -4
  6. package/src/deployManager/components/MachinesListPage.tsx +10 -2
  7. package/src/deployManager/components/ServiceDetailPage.tsx +13 -3
  8. package/src/deployManager/components/ServicesListPage.tsx +18 -6
  9. package/src/deployManager/machineApplyMainCode.ts +3 -3
  10. package/src/deployManager/machineSchema.ts +39 -0
  11. package/src/diagnostics/NodeViewer.tsx +2 -1
  12. package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +124 -123
  13. package/src/diagnostics/logs/IndexedLogs/BufferIndexHelpers.ts +83 -1
  14. package/src/diagnostics/logs/IndexedLogs/BufferListStreamer.ts +2 -0
  15. package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +21 -24
  16. package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +1 -1
  17. package/src/diagnostics/logs/IndexedLogs/FilePathSelector.tsx +186 -25
  18. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +284 -195
  19. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +312 -108
  20. package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +1 -1
  21. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +37 -7
  22. package/src/diagnostics/logs/errorNotifications2/errorNotifications2.ts +0 -0
  23. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +62 -35
  24. package/src/diagnostics/logs/lifeCycleAnalysis/test.ts +0 -180
  25. package/src/functional/limitProcessing.ts +39 -0
package/.cursorrules CHANGED
@@ -26,6 +26,7 @@ NEVER EVER pass state to qreact.Component as a template parameter. It should ALS
26
26
 
27
27
  t.lookup objects cannot be set with `this.state.someLookup = {}`. You can add keys or remove keys, and keys are added implicitly (`this.state.someLookup["someKey"] = { x: 1, y: 1 }`, always works, the key is automatically added if it doesn't exist). Removal uses delete `delete this.state.someLookup["someKey"]`.
28
28
 
29
+ Follow the rule of minimum scoping. If something can be a local variable, it should be a local variable, it shouldn't be a class variable. If it can be a class variable, it should be a class variable, not a state variable. Etc, etc. Keep it simple, reference things closer, and write less code.
29
30
 
30
31
  Try not to use "null", and instead always use "undefined".
31
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.357.0",
3
+ "version": "0.359.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",
@@ -56,6 +56,7 @@
56
56
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
57
57
  "pako": "^2.1.0",
58
58
  "peggy": "^5.0.6",
59
+ "querysub": "^0.357.0",
59
60
  "socket-function": "^1.1.2",
60
61
  "terser": "^5.31.0",
61
62
  "typesafecss": "^0.28.0",
@@ -1,4 +1,5 @@
1
1
  import { isNode } from "socket-function/src/misc";
2
+ import os from "os";
2
3
  import { fsExistsAsync, getSubFolder } from "../fs";
3
4
  import fs from "fs";
4
5
  import { blue, red, yellow } from "socket-function/src/formatting/logColors";
@@ -75,7 +76,7 @@ class ArchivesDisk {
75
76
  }
76
77
  }
77
78
 
78
- @measureFnc
79
+ // We don't time the file system writes as it's almost never blocking, it's not stopping our processing. And if it is stopping our processing, then we actually want to know the parent function which is calling it, and then we want it to just void it instead of awaiting it (And the only way to know the parent caller is to not time these functions).
79
80
  public async set(fileName: string, data: Buffer): Promise<void> {
80
81
  await this.init();
81
82
 
@@ -88,7 +89,6 @@ class ArchivesDisk {
88
89
 
89
90
  await fs.promises.writeFile(this.LOCAL_ARCHIVE_FOLDER + fileName, data);
90
91
  }
91
- @measureFnc
92
92
  public async append(fileName: string, data: Buffer): Promise<void> {
93
93
  await this.init();
94
94
 
@@ -101,7 +101,6 @@ class ArchivesDisk {
101
101
 
102
102
  await fs.promises.appendFile(this.LOCAL_ARCHIVE_FOLDER + fileName, data);
103
103
  }
104
- @measureFnc
105
104
  public async del(fileName: string): Promise<void> {
106
105
  await this.init();
107
106
  this.log(blue(`Deleting file ${fileName}`));
@@ -112,8 +111,16 @@ class ArchivesDisk {
112
111
  try {
113
112
  await fs.promises.rm(this.LOCAL_ARCHIVE_FOLDER + fileName, { recursive: true });
114
113
  } catch { }
115
- let dir = fileName.replaceAll("\\", "/").split("/").slice(0, -1).join("/");
116
- await this.gcDir(dir);
114
+ try {
115
+ let dir = fileName;
116
+ while (dir.length > 0) {
117
+ dir = dir.replaceAll("\\", "/").split("/").slice(0, -1).join("/");
118
+ if (dir.endsWith(":")) break;
119
+ let files = await fs.promises.readdir(this.LOCAL_ARCHIVE_FOLDER + dir);
120
+ if (files.length > 0) break;
121
+ await fs.promises.rmdir(this.LOCAL_ARCHIVE_FOLDER + dir);
122
+ }
123
+ } catch { }
117
124
  }
118
125
 
119
126
  public async setLargeFile(config: { path: string; getNextData(): Promise<Buffer | undefined>; }): Promise<void> {
@@ -180,7 +187,8 @@ class ArchivesDisk {
180
187
  * it all at once).
181
188
  * @fsErrorRetryCount Retries count for file system error (not user errors). Ex, too many open files, etc.
182
189
  */
183
- @measureFnc
190
+ // NOTE: I've commented out the gate measuring so we can determine what is taking the time. We might want to remove all the timing from this file, as if we find any timing this file is slow, it doesn't help us optimize at all, because we then need to figure out what is calling it.
191
+ //@measureFnc
184
192
  public async get(fileNameInput: string, config?: { range?: { start: number; end: number; }; retryCount?: number }): Promise<Buffer | undefined> {
185
193
  await this.init();
186
194
  this.log(blue(`Start read file ${fileNameInput}`));
@@ -433,5 +441,15 @@ export const getArchivesLocal = cache((domain: string): Archives => {
433
441
  archivesLocal.LOCAL_ARCHIVE_FOLDER = getSubFolder(domain).replaceAll("\\", "/");
434
442
  }
435
443
 
444
+ return archivesLocal;
445
+ });
446
+
447
+ export const getArchivesHome = cache((domain: string): Archives => {
448
+ const archivesLocal = new ArchivesDisk();
449
+
450
+ if (isNode()) {
451
+ archivesLocal.LOCAL_ARCHIVE_FOLDER = os.homedir() + "/querysub/" + domain + "/";
452
+ }
453
+
436
454
  return archivesLocal;
437
455
  });
@@ -36,13 +36,14 @@ export function createArchivesMemoryCache(
36
36
  // - Also, it means if there's a range read that happens, if that range read doesn't return enough bytes, for example, we try to read from 0 to 100, but we only get 50 bytes back, we can only cache the 0 to 50, and then if they read from 0 to 100 again, we can use the cache, but we have to also read 50 to 100 from the source, because the file might have gotten larger.
37
37
  fullyImmutable?: boolean;
38
38
  stats?: ArchivesMemoryCacheStats;
39
+ sizeCache?: Map<string, number>;
39
40
  }
40
41
  ): Archives {
41
42
  let {
42
43
  maxSize = 1024 * 1024 * 1024 * 4,
43
44
  maxCount = 1000 * 1000,
44
45
  fullyImmutable = false,
45
- extraReadSize = 1024
46
+ extraReadSize = 1024 * 1024
46
47
  } = config ?? {};
47
48
 
48
49
  // Cache structure: Map from path to sorted array of ranges
@@ -199,13 +200,34 @@ export function createArchivesMemoryCache(
199
200
  }
200
201
  });
201
202
 
203
+ function updateStats(cached: boolean, size: number | undefined) {
204
+ if (!config?.stats) {
205
+ return;
206
+ }
207
+ if (cached) {
208
+ config.stats.cachedReads++;
209
+ if (size !== undefined) {
210
+ config.stats.cachedReadSize += size;
211
+ }
212
+ } else {
213
+ config.stats.uncachedReads++;
214
+ if (size !== undefined) {
215
+ config.stats.uncachedReadSize += size;
216
+ }
217
+ }
218
+ config.stats.totalCacheSize = totalSize;
219
+ config.stats.totalCacheCount = lruArray.length;
220
+ }
221
+
202
222
  async function cachedGet(path: string, getConfig?: {
203
223
  range?: { start: number; end: number; };
204
224
  retryCount?: number;
205
225
  fastRead?: boolean;
206
226
  }): Promise<Buffer | undefined> {
207
227
  if (cacheDisabled) {
208
- return await archives.get(path, getConfig);
228
+ let result = await archives.get(path, getConfig);
229
+ updateStats(false, result?.length);
230
+ return result;
209
231
  }
210
232
 
211
233
  let range = getConfig?.range;
@@ -214,12 +236,15 @@ export function createArchivesMemoryCache(
214
236
  if (!range) {
215
237
  // If not fully immutable, don't cache entire file reads (might have appends)
216
238
  if (!fullyImmutable) {
217
- return await archives.get(path, getConfig);
239
+ let result = await archives.get(path, getConfig);
240
+ updateStats(false, result?.length);
241
+ return result;
218
242
  }
219
243
 
220
244
  // Get file info to determine size, then treat as range read
221
245
  let info = await archives.getInfo(path);
222
246
  if (!info) {
247
+ updateStats(false, undefined);
223
248
  return undefined;
224
249
  }
225
250
 
@@ -234,12 +259,20 @@ export function createArchivesMemoryCache(
234
259
  {
235
260
  for (let entry of cacheByPath.get(path) || []) {
236
261
  if (entry.start <= start && entry.end >= end) {
237
- return entry.data.slice(start - entry.start, end - entry.start);
262
+ let result = entry.data.slice(start - entry.start, end - entry.start);
263
+ updateStats(true, result.length);
264
+ return result;
238
265
  }
239
266
  }
240
267
  }
241
268
 
242
- let size = await archives.getInfo(path);
269
+ let cachedSize = config?.sizeCache?.get(path);
270
+ if (cachedSize === undefined) {
271
+ cachedSize = ((await archives.getInfo(path))?.size || 0);
272
+ config?.sizeCache?.set(path, cachedSize);
273
+ updateStats(false, undefined);
274
+ }
275
+ let size = cachedSize;
243
276
  if (!size) {
244
277
  return undefined;
245
278
  }
@@ -250,7 +283,7 @@ export function createArchivesMemoryCache(
250
283
  let aligned = alignRange(start, end);
251
284
 
252
285
  let readStart = aligned.start;
253
- let readEnd = Math.min(aligned.end, size.size);
286
+ let readEnd = Math.min(aligned.end, size);
254
287
 
255
288
  let overlapping = findOverlappingEntries(path, start, end);
256
289
 
@@ -283,6 +316,7 @@ export function createArchivesMemoryCache(
283
316
  });
284
317
 
285
318
  if (!data) {
319
+ updateStats(false, undefined);
286
320
  return undefined;
287
321
  }
288
322
 
@@ -329,17 +363,7 @@ export function createArchivesMemoryCache(
329
363
  let result = coveringEntry.data.slice(offsetStart, offsetEnd);
330
364
 
331
365
  // Update statistics at the end when we know the final result
332
- if (config?.stats) {
333
- if (didRead) {
334
- config.stats.uncachedReads++;
335
- config.stats.uncachedReadSize += result.length;
336
- } else {
337
- config.stats.cachedReads++;
338
- config.stats.cachedReadSize += result.length;
339
- }
340
- config.stats.totalCacheSize = totalSize;
341
- config.stats.totalCacheCount = lruArray.length;
342
- }
366
+ updateStats(!didRead, result.length);
343
367
 
344
368
  return result;
345
369
  }
@@ -9,6 +9,8 @@ import { ATag, Anchor } from "../../library-components/ATag";
9
9
  import { ShowMore } from "../../library-components/ShowMore";
10
10
  import { filterParam } from "../../diagnostics/logs/FastArchiveViewer";
11
11
  import { managementPageURL } from "../../diagnostics/managementPages";
12
+ import { t } from "../../2-proxy/schema2";
13
+ import { Querysub } from "../../4-querysub/QuerysubController";
12
14
 
13
15
  export class MachineDetailPage extends qreact.Component {
14
16
  render() {
@@ -18,12 +20,14 @@ export class MachineDetailPage extends qreact.Component {
18
20
  let controller = MachineServiceController(SocketFunction.browserNodeId());
19
21
  let machineInfo = controller.getMachineInfo(selectedMachineId);
20
22
  let serviceList = controller.getServiceList();
23
+ let machineConfig = controller.getMachineConfig(selectedMachineId);
21
24
 
22
25
  if (controller.isAnyLoading()) return <div>Loading machine info...</div>;
23
26
  if (!machineInfo) return <div>Machine not found</div>;
24
27
  if (!serviceList) return <div>Service list not found</div>;
25
28
 
26
- const machine = machineInfo; // Create const reference for type safety
29
+ const machine = machineInfo;
30
+ const isDisabled = machineConfig?.disabled || false;
27
31
 
28
32
  // Get all service configs that target this machine
29
33
  let relevantServiceConfigs = new Map<string, ServiceConfig>();
@@ -47,12 +51,49 @@ export class MachineDetailPage extends qreact.Component {
47
51
 
48
52
  const isMachineDead = Date.now() - machine.heartbeat > (MACHINE_RESYNC_INTERVAL * 4);
49
53
 
54
+ let failingServices = Object.keys(machine.services).filter(serviceId => {
55
+ return machine.services[serviceId].errorFromLastRun;
56
+ });
57
+
58
+ let backgroundColor = (
59
+ isDisabled && css.hsl(0, 0, 50)
60
+ || failingServices.length > 0 && css.hsl(0, 50, 60)
61
+ || isMachineDead && css.hsl(45, 80, 80)
62
+ || css.hsl(0, 0, 100)
63
+ );
64
+
50
65
  return <div className={css.vbox(16)}>
51
- <div className={css.hbox(12)}>
66
+ <div className={css.hbox(12).pad2(16).bord2(0, 0, 20) + backgroundColor}>
52
67
  <h2 className={css.flexGrow(1)}>{selectedMachineId}</h2>
53
68
  {isMachineDead && <div className={css.colorhsl(0, 80, 60)}>
54
69
  ⚠️ Machine is likely dead
55
70
  </div>}
71
+ {isDisabled && <div className={css.colorhsl(30, 80, 60).pad2(8, 4).bord2(30, 80, 60).hsl(30, 80, 95)}>
72
+ ⚠️ Machine is disabled
73
+ </div>}
74
+ </div>
75
+
76
+ <div className={css.hbox(12).pad2(16).bord2(0, 0, 20).hsl(0, 0, 95)}>
77
+ <div className={css.flexGrow(1).vbox(4)}>
78
+ <div className={css.fontSize(16).colorhsl(0, 0, 20)}>
79
+ <b>Machine Status</b>
80
+ </div>
81
+ <div>
82
+ {isDisabled && "When disabled, services will not be deployed to this machine. Re-enabling will restore all service assignments." || "This machine is enabled and available for service deployments."}
83
+ </div>
84
+ </div>
85
+ <button
86
+ className={css.pad2(12, 8).button.bord2(0, 0, 20) + (isDisabled ? css.hsl(120, 60, 90) : css.hsl(30, 80, 90))}
87
+ onClick={() => {
88
+ Querysub.onCommitFinished(async () => {
89
+ await controller.setMachineConfig.promise(selectedMachineId, {
90
+ machineId: selectedMachineId,
91
+ disabled: !isDisabled
92
+ });
93
+ });
94
+ }}>
95
+ {isDisabled && "Enable Machine" || "Disable Machine"}
96
+ </button>
56
97
  </div>
57
98
 
58
99
  <div className={css.vbox(12)}>
@@ -77,8 +118,8 @@ export class MachineDetailPage extends qreact.Component {
77
118
  <div className={css.vbox(10)}>
78
119
  {Object.entries(machine.info).map(([key, value]) => (
79
120
  <div key={key} className={css.hbox(10).hsla(0, 0, 0, 0.1).pad2(10, 2)}>
80
- <b>{key}:</b>
81
- <ShowMore className={css.whiteSpace("pre-wrap")} maxHeight={40}>
121
+ <b className={css.flexShrink0}>{key}:</b>
122
+ <ShowMore className={css.whiteSpace("pre-wrap")} maxHeight={80}>
82
123
  {(() => {
83
124
  if (typeof value === "object") {
84
125
  return <div className={css.pad2(6, 2).hsla(0, 0, 0, 0.1).relative}>
@@ -28,6 +28,8 @@ export class MachinesListPage extends qreact.Component {
28
28
  let machines = machineList.map(machineId => [machineId, controller.getMachineInfo(machineId)] as const).filter(x => x[1]);
29
29
  sort(machines, x => -(x[1]?.heartbeat || 0));
30
30
 
31
+ let machineConfigs = controller.getMachineConfigList();
32
+
31
33
  const selectedMachineIds = Object.keys(this.state.selectedForDeletion);
32
34
  const hasSelectedMachines = selectedMachineIds.length > 0;
33
35
  const DEAD_MACHINE_THRESHOLD = MACHINE_RESYNC_INTERVAL * 4;
@@ -121,6 +123,8 @@ export class MachinesListPage extends qreact.Component {
121
123
  const serviceCount = Object.keys(machineInfo.services).length;
122
124
  const isMachineDead = Date.now() - machineInfo.heartbeat > (MACHINE_RESYNC_INTERVAL * 4);
123
125
  const isSelected = this.state.selectedForDeletion[machineId];
126
+ const machineConfig = machineConfigs?.find(x => x.machineId === machineId);
127
+ const isDisabled = machineConfig?.disabled || false;
124
128
 
125
129
  let failingServices = Object.keys(machineInfo.services).filter(serviceId => {
126
130
  return machineInfo.services[serviceId].errorFromLastRun;
@@ -136,8 +140,9 @@ export class MachinesListPage extends qreact.Component {
136
140
  className={
137
141
  css.pad2(12).bord2(0, 0, 20).button
138
142
  + (
139
- failingServices.length > 0 && css.hsl(0, 50, 60)
140
- || isMachineDead && css.hsl(0, 0, 50)
143
+ isDisabled && css.hsl(0, 0, 50)
144
+ || failingServices.length > 0 && css.hsl(0, 50, 60)
145
+ || isMachineDead && css.hsl(45, 80, 80)
141
146
  || css.hsl(0, 0, 100)
142
147
  )
143
148
  + (this.state.isDeleteMode && isSelected && css.bord2(200, 80, 60, 2))
@@ -165,6 +170,9 @@ export class MachinesListPage extends qreact.Component {
165
170
  />
166
171
  </div>
167
172
  )}
173
+ {isDisabled && <div className={css.colorhsl(30, 80, 60).pad2(4, 2).bord2(30, 80, 60).hsl(30, 80, 95)}>
174
+ 🚫 Disabled
175
+ </div>}
168
176
  {isMachineDead && <div className={css.colorhsl(0, 80, 50)}>
169
177
  ⚠️ Machine is likely dead
170
178
  </div>}
@@ -157,9 +157,11 @@ export class ServiceDetailPage extends qreact.Component {
157
157
  let index = nextIndexes.get(machineId) || 0;
158
158
  nextIndexes.set(machineId, index + 1);
159
159
  let machineInfo = controller.getMachineInfo(machineId);
160
+ let machineConfig = controller.getMachineConfig(machineId);
160
161
  const serviceInfo = machineInfo?.services[selectedServiceId || ""];
161
162
  const isMachineDead = machineInfo ? Date.now() - machineInfo.heartbeat > (MACHINE_RESYNC_INTERVAL * 4) : true;
162
163
  const hasError = serviceInfo?.errorFromLastRun;
164
+ const isDisabled = machineConfig?.disabled || false;
163
165
 
164
166
  return {
165
167
  machineId,
@@ -167,11 +169,12 @@ export class ServiceDetailPage extends qreact.Component {
167
169
  serviceInfo,
168
170
  isMachineDead,
169
171
  hasError,
172
+ isDisabled,
170
173
  heartbeat: machineInfo?.heartbeat || 0,
171
174
  index
172
175
  };
173
176
  });
174
- sort(machineStatuses, x => (x.hasError ? 0 : x.isMachineDead ? 1 : 2) * 1000000 + x.heartbeat);
177
+ sort(machineStatuses, x => (x.isDisabled ? 3 : x.hasError ? 0 : x.isMachineDead ? 1 : 2) * 1000000 + x.heartbeat);
175
178
  let now = Date.now();
176
179
 
177
180
  let machines = (controller.getMachineList() || []).map(x => controller.getMachineInfo(x)).filter(isDefined);
@@ -253,11 +256,13 @@ export class ServiceDetailPage extends qreact.Component {
253
256
  {configT.parameters.deploy && <div className={css.vbox(8).fillWidth}>
254
257
  <h3>Deployed Machines ({config.machineIds.length})</h3>
255
258
  <div className={css.vbox(4).fillWidth}>
256
- {machineStatuses.map(({ machineId, machineInfo, serviceInfo, isMachineDead, hasError, index }) => {
259
+ {machineStatuses.map(({ machineId, machineInfo, serviceInfo, isMachineDead, hasError, isDisabled, index }) => {
257
260
  if (!machineInfo) return <div key={machineId}>Loading {machineId}...</div>;
258
261
 
259
262
  let backgroundColor = css.hsl(0, 0, 100); // Default: white
260
- if (hasError) {
263
+ if (isDisabled) {
264
+ backgroundColor = css.hsl(0, 0, 50); // Gray for disabled machines
265
+ } else if (hasError) {
261
266
  backgroundColor = css.hsl(0, 70, 90); // Red-ish for errors
262
267
  } else if (isMachineDead) {
263
268
  backgroundColor = css.hsl(0, 0, 50); // Gray for dead machines
@@ -281,6 +286,11 @@ export class ServiceDetailPage extends qreact.Component {
281
286
  {machineId} ({machineInfo.info["getExternalIP"]})
282
287
  </div>
283
288
  </div>
289
+ {isDisabled && (
290
+ <div className={css.colorhsl(0, 0, 30)}>
291
+ 🚫 Machine Disabled
292
+ </div>
293
+ )}
284
294
  {isMachineDead && (
285
295
  <div className={css.colorhsl(0, 80, 50)}>
286
296
  ⚠️ Machine Dead
@@ -21,6 +21,7 @@ export class ServicesListPage extends qreact.Component {
21
21
  render() {
22
22
  let controller = MachineServiceController(SocketFunction.browserNodeId());
23
23
  let serviceList = controller.getServiceList();
24
+ let machineConfigList = controller.getMachineConfigList();
24
25
 
25
26
  if (!serviceList) return <div>Loading services...</div>;
26
27
 
@@ -28,6 +29,7 @@ export class ServicesListPage extends qreact.Component {
28
29
  sort(services, x => -(x[1]?.info.lastUpdatedTime || Date.now()) - (x[1]?.parameters.deploy && (Number.MAX_SAFE_INTEGER / 2) || 0));
29
30
 
30
31
  let getMachineInfo = cache((machineId: string) => controller.getMachineInfo(machineId));
32
+ let getMachineConfig = cache((machineId: string) => controller.getMachineConfig(machineId));
31
33
 
32
34
  let keyCounts = new Map<string, number>();
33
35
  for (let [serviceId, config] of services) {
@@ -75,24 +77,34 @@ export class ServicesListPage extends qreact.Component {
75
77
  <div className={css.vbox(8)}>
76
78
  {services.map(([serviceId, config]) => {
77
79
  if (!config) return <div key={serviceId}>Config is broken? Missing value for service? Is the file corrupted?</div>;
78
- let failingMachines = config.machineIds.filter(machineId => {
80
+
81
+ let disabledMachines = config.machineIds.filter(machineId => {
82
+ let machineConfig = getMachineConfig(machineId);
83
+ return machineConfig?.disabled;
84
+ });
85
+ let enabledMachineIds = config.machineIds.filter(machineId => {
86
+ let machineConfig = getMachineConfig(machineId);
87
+ return !machineConfig?.disabled;
88
+ });
89
+
90
+ let failingMachines = enabledMachineIds.filter(machineId => {
79
91
  let machineInfo = getMachineInfo(machineId);
80
92
  return machineInfo?.services[serviceId]?.errorFromLastRun;
81
93
  });
82
- let runningMachines = config.machineIds.filter(machineId => {
94
+ let runningMachines = enabledMachineIds.filter(machineId => {
83
95
  let machineInfo = getMachineInfo(machineId);
84
96
  let serviceInfo = machineInfo?.services[serviceId];
85
97
  return serviceInfo && !serviceInfo.errorFromLastRun;
86
98
  });
87
- let missingMachines = config.machineIds.filter(machineId => {
99
+ let missingMachines = enabledMachineIds.filter(machineId => {
88
100
  let machineInfo = getMachineInfo(machineId);
89
101
  return !machineInfo?.services[serviceId];
90
102
  });
91
- let totalLaunches = config.machineIds.reduce((acc, machineId) => {
103
+ let totalLaunches = enabledMachineIds.reduce((acc, machineId) => {
92
104
  let machineInfo = getMachineInfo(machineId);
93
105
  return acc + (machineInfo?.services[serviceId]?.totalTimesLaunched || 0);
94
106
  }, 0);
95
- let unknown = config.machineIds.length - runningMachines.length - failingMachines.length - missingMachines.length;
107
+ let unknown = enabledMachineIds.length - runningMachines.length - failingMachines.length - missingMachines.length;
96
108
  let duplicateKey = (keyCounts.get(config.parameters.key || "") || 0) > 1;
97
109
  return <div className={css.hbox(10)}>
98
110
  <Anchor noStyles key={serviceId}
@@ -118,7 +130,7 @@ export class ServicesListPage extends qreact.Component {
118
130
  {config.parameters.key}
119
131
  </div>
120
132
  <div>
121
- {config.machineIds.length} configured {failingMachines.length > 0 && `(${failingMachines.length} failing)`} {missingMachines.length > 0 && `(${missingMachines.length} machine hasn't run service yet)`} {unknown > 0 && `(${unknown} unknown)`} • {totalLaunches} launches •
133
+ {enabledMachineIds.length} configured {disabledMachines.length > 0 && `(+${disabledMachines.length} on disabled machines)`} {failingMachines.length > 0 && `(${failingMachines.length} failing)`} {missingMachines.length > 0 && `(${missingMachines.length} machine hasn't run service yet)`} {unknown > 0 && `(${unknown} unknown)`} • {totalLaunches} launches •
122
134
  Deploy: {config.parameters.deploy ? "enabled" : "disabled"}
123
135
  </div>
124
136
  </div>
@@ -4,7 +4,7 @@ import { measureWrap } from "socket-function/src/profiling/measure";
4
4
  import { getOwnMachineId } from "../-a-auth/certs";
5
5
  import { forceRemoveNode, getOurNodeId, getOurNodeIdAssert } from "../-f-node-discovery/NodeDiscovery";
6
6
  import { Querysub } from "../4-querysub/QuerysubController";
7
- import { MACHINE_RESYNC_INTERVAL, MachineServiceControllerBase, MachineInfo, ServiceConfig, serviceConfigs, SERVICE_FOLDER, machineInfos, SERVICE_NODE_FILE_NAME } from "./machineSchema";
7
+ import { MACHINE_RESYNC_INTERVAL, MachineServiceControllerBase, MachineInfo, ServiceConfig, serviceConfigs, SERVICE_FOLDER, machineInfos, SERVICE_NODE_FILE_NAME, getEffectiveServiceConfigs } from "./machineSchema";
8
8
  import { runPromise } from "../functional/runCommand";
9
9
  import { getExternalIP } from "socket-function/src/networking";
10
10
  import { errorToUndefined, errorToUndefinedSilent } from "../errors";
@@ -579,7 +579,7 @@ async function quickIsOutdated() {
579
579
  return true;
580
580
  }
581
581
  }
582
- let configs = await serviceConfigs.values();
582
+ let configs = await getEffectiveServiceConfigs();
583
583
  let relevantConfigs = configs.filter(config => config.machineIds.includes(machineId)).filter(x => x.parameters.deploy);
584
584
  let screens = await getScreenState();
585
585
  let root = os.homedir() + "/" + SERVICE_FOLDER;
@@ -608,7 +608,7 @@ async function quickIsOutdated() {
608
608
  const resyncServicesBase = runInSerial(measureWrap(async function resyncServices() {
609
609
  console.log(magenta("Resyncing services"));
610
610
  let machineId = getOwnMachineId();
611
- let configs = await serviceConfigs.values();
611
+ let configs = await getEffectiveServiceConfigs();
612
612
  let relevantConfigs = configs.filter(config => config.machineIds.includes(machineId)).filter(x => x.parameters.deploy);
613
613
 
614
614
  let machineInfo = await getLiveMachineInfo();
@@ -93,8 +93,13 @@ export type ServiceConfig = {
93
93
  lastUpdatedTime: number;
94
94
  };
95
95
  };
96
+ export type MachineConfig = {
97
+ machineId: string;
98
+ disabled: boolean;
99
+ };
96
100
  export const machineInfos = archiveJSONT<MachineInfo>(() => nestArchives("machines/machine-heartbeats/", getArchivesBackblaze(getDomain())));
97
101
  export const serviceConfigs = archiveJSONT<ServiceConfig>(() => nestArchives("machines/service-configs/", getArchivesBackblaze(getDomain())));
102
+ export const machineConfigs = archiveJSONT<MachineConfig>(() => nestArchives("machines/machine-configs/", getArchivesBackblaze(getDomain())));
98
103
 
99
104
  export function doRegisterNodeForMachineCleanup() {
100
105
  if (isNode()) {
@@ -153,6 +158,16 @@ export class MachineServiceControllerBase {
153
158
  return await serviceConfigs.get(serviceId);
154
159
  }
155
160
 
161
+ public async getMachineConfigList() {
162
+ return await machineConfigs.values();
163
+ }
164
+ public async getMachineConfig(machineId: string): Promise<MachineConfig | undefined> {
165
+ return await machineConfigs.get(machineId);
166
+ }
167
+ public async setMachineConfig(machineId: string, config: MachineConfig) {
168
+ await machineConfigs.set(machineId, config);
169
+ }
170
+
156
171
  private async notifyMachines(newMachineIds: string[], oldMachineIds: string[]) {
157
172
  let addedNodeIds = new Set<string>();
158
173
  let removedNodeIds = new Set<string>();
@@ -358,6 +373,24 @@ export class MachineServiceControllerBase {
358
373
  });
359
374
  }
360
375
  }
376
+
377
+ export async function getEffectiveServiceConfigs(): Promise<ServiceConfig[]> {
378
+ let configs = await serviceConfigs.values();
379
+ let allMachineConfigs = await machineConfigs.values();
380
+
381
+ let disabledMachineIds = new Set<string>();
382
+ for (let machineConfig of allMachineConfigs) {
383
+ if (machineConfig.disabled) {
384
+ disabledMachineIds.add(machineConfig.machineId);
385
+ }
386
+ }
387
+
388
+ return configs.map(config => ({
389
+ ...config,
390
+ machineIds: config.machineIds.filter(machineId => !disabledMachineIds.has(machineId)),
391
+ }));
392
+ }
393
+
361
394
  let deployWatchers = new Set<DeployProgress>();
362
395
  class DeployProgressControllerBase {
363
396
  async onDeployProgress(config: { section: string; progress: number; }) {
@@ -401,6 +434,9 @@ export const MachineServiceController = getSyncedController(
401
434
  setMachineInfo: {},
402
435
  getServiceList: {},
403
436
  getServiceConfig: {},
437
+ getMachineConfigList: {},
438
+ getMachineConfig: {},
439
+ setMachineConfig: {},
404
440
  addServiceConfig: {},
405
441
  setServiceConfigs: {},
406
442
  getServiceConfigType: {},
@@ -422,6 +458,7 @@ export const MachineServiceController = getSyncedController(
422
458
  deleteMachineIds: ["MachineInfo", "MachineInfoList"],
423
459
  addMachineInfo: ["MachineInfo", "MachineInfoList"],
424
460
  setMachineInfo: ["MachineInfo"],
461
+ setMachineConfig: ["MachineConfig", "MachineConfigList"],
425
462
  addServiceConfig: ["ServiceConfig", "ServiceConfigList"],
426
463
  setServiceConfigs: ["ServiceConfig"],
427
464
  deleteServiceConfig: ["ServiceConfig", "ServiceConfigList"],
@@ -432,6 +469,8 @@ export const MachineServiceController = getSyncedController(
432
469
  reads: {
433
470
  getMachineList: ["MachineInfoList"],
434
471
  getMachineInfo: ["MachineInfo"],
472
+ getMachineConfigList: ["MachineConfigList"],
473
+ getMachineConfig: ["MachineConfig"],
435
474
  getServiceList: ["ServiceConfigList"],
436
475
  getServiceConfig: ["ServiceConfig"],
437
476
  getGitInfo: ["gitInfo"],
@@ -80,6 +80,7 @@ export class NodeViewer extends qreact.Component {
80
80
  Querysub.commit(() => this.state.nodeIds = nodeIds);
81
81
 
82
82
  let ourIP = await controller.getCallerIP();
83
+ let ourExternalIP = await getExternalIP();
83
84
 
84
85
  await Promise.allSettled(nodeIds.map(async nodeId => {
85
86
  let time = Date.now();
@@ -97,7 +98,7 @@ export class NodeViewer extends qreact.Component {
97
98
  data.live_entryPoint = await controller.live_getEntryPoint(nodeId);
98
99
  data.live_authorityPaths = await controller.live_getAuthorityPaths(nodeId);
99
100
  data.ip = await controller.getNodeIP(nodeId);
100
- if (data.ip === ourIP || data.ip === "127.0.0.1") {
101
+ if (data.ip === ourExternalIP || data.ip === ourIP) {
101
102
  data.devToolsURL = await controller.getInspectURL(nodeId);
102
103
  if (data.devToolsURL) {
103
104
  data.devToolsURL = data.devToolsURL + "<inspect>";