querysub 0.313.0 → 0.315.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.313.0",
3
+ "version": "0.315.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",
@@ -28,6 +28,7 @@ import { magenta, yellow } from "socket-function/src/formatting/logColors";
28
28
  import { timeInMinute, timeInSecond } from "socket-function/src/misc";
29
29
  import { nodeDiscoveryShutdown } from "../-f-node-discovery/NodeDiscovery";
30
30
  import { shutdown } from "../diagnostics/periodic";
31
+ import { formatDateTime } from "socket-function/src/formatting/format";
31
32
 
32
33
  let publicPort = -1;
33
34
 
@@ -65,6 +66,8 @@ export async function getSNICerts(config: {
65
66
  },
66
67
  };
67
68
 
69
+ await publishEdgeDomain();
70
+
68
71
  return certs;
69
72
  }
70
73
 
@@ -93,7 +96,7 @@ const certUpdateLoop = lazy(() => {
93
96
  logErrors((async () => {
94
97
  let firstLoop = true;
95
98
  while (true) {
96
- let curCert = await getCertFromRemote();
99
+ let curCert = await getHTTPSKeyCert(getDomain());
97
100
  if (!curCert) {
98
101
  throw new Error(`Internal error, certUpdateLoop called before lastPromise was set`);
99
102
  }
@@ -102,14 +105,9 @@ const certUpdateLoop = lazy(() => {
102
105
  let expirationTime = +new Date(certObj.validity.notAfter);
103
106
  let createTime = +new Date(certObj.validity.notBefore);
104
107
 
105
- // If 75% of the lifetime has passed, renew the cert
108
+ // If 75% of the lifetime has passed, getHTTPSKeyCert should have updated it, so wait until that, then get the new cert, and update our watchers (which should be the servers).
106
109
  let renewDate = createTime + (expirationTime - createTime) * 0.75;
107
110
  let timeToExpire = renewDate - Date.now();
108
- if (timeToExpire < 0) {
109
- console.log(`HTTPS certificate is looking too old. Renewing from remote.`);
110
- getCertFromRemote = createGetCertFromRemote();
111
- continue;
112
- }
113
111
 
114
112
  if (!firstLoop) {
115
113
  for (let callback of callbacks) {
@@ -118,43 +116,19 @@ const certUpdateLoop = lazy(() => {
118
116
  }
119
117
  firstLoop = false;
120
118
 
119
+ if (timeToExpire < 0) {
120
+ console.warn(`getHTTPSKeyCert gave as an almost expired. It is not supposed to do this. It should have updated it by now... Expires on ${formatDateTime(expirationTime)}`);
121
+ timeToExpire = timeInMinute * 15;
122
+ }
121
123
  // Max timeout a signed integer, but lower is fine too
122
- timeToExpire = Math.min(timeToExpire, 2 ** 30);
123
- console.log(`Certicates up to date, renewing on ${new Date(Date.now() + timeToExpire)}`);
124
+ timeToExpire = Math.min(Math.floor(timeToExpire), 2 ** 30);
124
125
  await delay(timeToExpire);
125
- console.log(`Woke up to renew`);
126
+ console.log(`Woke up to propagate new certs`);
126
127
  }
127
128
  })());
128
129
  });
129
130
 
130
- function createGetCertFromRemote() {
131
- return lazy(async () => {
132
- return backoffRetryLoop(async () => {
133
- // Skip the remote call if we have DNS write permissions, to
134
- // make bootstrapping easier.
135
- if (await hasDNSWritePermissions()) {
136
- let ip = SocketFunction.mountedIP;
137
- if (ip === "0.0.0.0") {
138
- ip = await getExternalIP();
139
- }
140
- return await getHTTPSKeyCertInner(ip);
141
- }
142
-
143
- let edgeNodeId = await getControllerNodeId(EdgeCertController);
144
- if (!edgeNodeId) {
145
- throw new Error("No EdgeCertController found");
146
- }
147
- return await EdgeCertController.nodes[edgeNodeId].getHTTPSKeyCert();
148
- });
149
- });
150
- }
151
- let getCertFromRemote = createGetCertFromRemote();
152
131
 
153
- // NOTE: Only works if the machine is already listening. Needed for some special websocket
154
- // stuff related to debugging.
155
- export function debugGetRawEdgeCert() {
156
- return getCertFromRemote();
157
- }
158
132
 
159
133
  let callbacks: ((newCertPair: { cert: string; key: string }) => void)[] = [];
160
134
  async function EdgeCertController_watchHTTPSKeyCert(
@@ -163,7 +137,7 @@ async function EdgeCertController_watchHTTPSKeyCert(
163
137
  certUpdateLoop();
164
138
 
165
139
  callbacks.push(callback);
166
- let certPair = await getCertFromRemote();
140
+ let certPair = await getHTTPSKeyCert(getDomain());
167
141
  callback(certPair);
168
142
  }
169
143
 
@@ -194,7 +168,7 @@ const runEdgeDomainAliveLoop = lazy(() => {
194
168
  // NOTE: Our DNS TTL is 1 minute, which means no matter how fast we poll,
195
169
  // we can't get below that. Of course the worst case is that + our poll rate,
196
170
  // but still, this means there is less and less benefit the lower this value is.
197
- runInfinitePoll(timeInMinute, checkEdgeDomainsAlive);
171
+ runInfinitePoll(timeInMinute * 3, checkEdgeDomainsAlive);
198
172
  });
199
173
  async function checkEdgeDomainsAlive() {
200
174
  if (isNoNetwork()) return;
@@ -228,95 +202,62 @@ async function checkEdgeDomainsAlive() {
228
202
  }
229
203
  }
230
204
 
231
- async function getHTTPSKeyCertInner(callerIP: string) {
232
- let cert = await getBaseCert();
233
- // If the cert is 50% expired generate a new one
234
- let certObj = parseCert(cert.cert);
235
-
236
- // Get expiration date
237
- let expirationTime = +new Date(certObj.validity.notAfter);
238
- let createTime = +new Date(certObj.validity.notBefore);
239
-
240
- // If 50% of the lifetime has passed, renew the cert
241
- let renewDate = createTime + (expirationTime - createTime) * 0.5;
242
- if (renewDate < Date.now()) {
243
- console.log(`HTTPS certificate is looking too old, forcefully renewing`);
244
- getHTTPSKeyCert.clear(getDomain());
245
- getBaseCert = createGetBaseCert();
246
- cert = await getBaseCert();
205
+ async function publishEdgeDomain() {
206
+ let callerIP = SocketFunction.mountedIP;
207
+ if (callerIP === "0.0.0.0") {
208
+ callerIP = await getExternalIP();
209
+ }
210
+ if (!callerIP) {
211
+ console.warn(`publishEdgeDomain called before SocketFunction.mount was called, so we don't know the public ip.`);
212
+ return;
247
213
  }
248
-
249
214
  // IMPORTANT! We have to set our A record AFTER we create our cert, otherwise we might wait a while
250
215
  // with our A record public while we create our cert.
251
216
  runEdgeDomainAliveLoop();
252
217
  const edgeDomain = getDomain();
253
- if (callerIP && !isBootstrapOnly()) {
254
- try {
255
- let promises: Promise<void>[] = [];
256
- let existingIPs = await getRecords("A", edgeDomain);
257
- if (!isPublic()) {
218
+ try {
219
+ let promises: Promise<void>[] = [];
220
+ let existingIPs = await getRecords("A", edgeDomain);
221
+ if (!isPublic()) {
222
+ if (existingIPs.length === 0) {
223
+ promises.push(addRecord("A", edgeDomain, callerIP));
224
+ } else if (existingIPs.length === 1 && existingIPs[0] === "127.0.0.1") {
225
+ // Good, do nothing
226
+ } else {
227
+ // Don't remove the public servers, this is probably just us accidentally running a
228
+ // test script for a public domain.
229
+ /*
230
+ // Maybe they took down all the public servers?
231
+ await removeDeadARecords();
232
+ existingIPs = await getRecords("A", edgeDomain);
258
233
  if (existingIPs.length === 0) {
259
- promises.push(addRecord("A", edgeDomain, callerIP));
260
- } else if (existingIPs.length === 1 && existingIPs[0] === "127.0.0.1") {
261
- // Good, do nothing
234
+ await addRecord("A", edgeDomain, callerIP);
262
235
  } else {
263
- // Don't remove the public servers, this is probably just us accidentally running a
264
- // test script for a public domain.
265
- /*
266
- // Maybe they took down all the public servers?
267
- await removeDeadARecords();
268
- existingIPs = await getRecords("A", edgeDomain);
269
- if (existingIPs.length === 0) {
270
- await addRecord("A", edgeDomain, callerIP);
271
- } else {
272
- await addRecord("A", "127-0-0-1." + edgeDomain, "127.0.0.1");
273
- console.warn(`Tried to serve an edge node on the local domain, but SocketFunction.mount did NOT specify a public ip (ex, { ip: "0.0.0.0" }) AND there are already existing public servers. You can't load balance between real ips and 127.0.0.1! ${existingIPs.join(", ")}. You will need to use 127-0-0-1.${edgeDomain} to access the local server (instead of just ${edgeDomain}).`);
274
- }
275
- */
276
- console.log(yellow(`Current process is not marked as public, but machine previous had public services. NOT setting ${edgeDomain} to 127.0.0.1, as this would make the public services inaccessible. Assuming the current process is for development, I recommend using 127-0-0-1.${edgeDomain} or 127-0-0-1.${edgeDomain} to access the server.`));
236
+ await addRecord("A", "127-0-0-1." + edgeDomain, "127.0.0.1");
237
+ console.warn(`Tried to serve an edge node on the local domain, but SocketFunction.mount did NOT specify a public ip (ex, { ip: "0.0.0.0" }) AND there are already existing public servers. You can't load balance between real ips and 127.0.0.1! ${existingIPs.join(", ")}. You will need to use 127-0-0-1.${edgeDomain} to access the local server (instead of just ${edgeDomain}).`);
277
238
  }
278
- } else {
279
- if (existingIPs.includes("127.0.0.1")) {
280
- console.log(magenta(`Switching from local development to production development (removing A record for 127.0.0.1, and using a real ip)`));
281
- promises.push(deleteRecord("A", edgeDomain, "127.0.0.1"));
282
- }
283
- promises.push(addRecord("A", edgeDomain, callerIP, "proxied"));
284
- runInfinitePoll(timeInMinute * 1, async () => {
285
- let ips = await getRecords("A", edgeDomain);
286
- if (!ips.includes(callerIP)) {
287
- console.error(`Our A record for ${edgeDomain} is no longer pointing to our ip (${callerIP}, as it is now pointing to ${JSON.stringify(ips)}). Terminating, hopefully when we restart we will fix it. This SHOULDN'T happen often!`);
288
- await shutdown();
289
- }
290
- });
239
+ */
240
+ console.log(yellow(`Current process is not marked as public, but machine previous had public services. NOT setting ${edgeDomain} to 127.0.0.1, as this would make the public services inaccessible. Assuming the current process is for development, I recommend using 127-0-0-1.${edgeDomain} or 127-0-0-1.${edgeDomain} to access the server.`));
241
+ }
242
+ } else {
243
+ if (existingIPs.includes("127.0.0.1")) {
244
+ console.log(magenta(`Switching from local development to production development (removing A record for 127.0.0.1, and using a real ip)`));
245
+ promises.push(deleteRecord("A", edgeDomain, "127.0.0.1"));
291
246
  }
292
- promises.push(addRecord("A", "127-0-0-1." + edgeDomain, "127.0.0.1"));
293
- // Add records in parallel, so we can wait for DNS propagation in parallel
294
- await Promise.all(promises);
295
- } catch (e) {
296
- console.error(`Error updating DNS records, continuing without updating them`, e);
247
+ promises.push(addRecord("A", edgeDomain, callerIP, "proxied"));
248
+ runInfinitePoll(timeInMinute * 1, async () => {
249
+ let ips = await getRecords("A", edgeDomain);
250
+ if (!ips.includes(callerIP)) {
251
+ console.error(`Our A record for ${edgeDomain} is no longer pointing to our ip (${callerIP}, as it is now pointing to ${JSON.stringify(ips)}). Terminating, hopefully when we restart we will fix it. This SHOULDN'T happen often!`);
252
+ await shutdown();
253
+ }
254
+ });
297
255
  }
298
- }
299
-
300
- return cert;
301
- }
302
-
303
- class EdgeCertControllerBase {
304
- public async getHTTPSKeyCert() {
305
- // Ensure the a record is subscribed
306
- const caller = SocketFunction.getCaller();
307
- let callerIP = getNodeIdIP(caller.nodeId);
308
- return await getHTTPSKeyCertInner(callerIP);
256
+ promises.push(addRecord("A", "127-0-0-1." + edgeDomain, "127.0.0.1"));
257
+ // Add records in parallel, so we can wait for DNS propagation in parallel
258
+ await Promise.all(promises);
259
+ } catch (e) {
260
+ console.error(`Error updating DNS records, continuing without updating them`, e);
309
261
  }
310
262
  }
311
263
 
312
-
313
- const EdgeCertController = SocketFunction.register(
314
- "EdgeCertController-694c925b-fb10-4656-aed0-b53a48ded548",
315
- new EdgeCertControllerBase(),
316
- () => ({
317
- getHTTPSKeyCert: {},
318
- }),
319
- () => ({
320
- hooks: [requiresNetworkTrustHook],
321
- })
322
- );
@@ -40,6 +40,12 @@ export const getHTTPSKeyCert = cache(async (domain: string): Promise<{ key: stri
40
40
  console.log(magenta(`Renewing domain ${domain} (renew target is ${formatDateTime(renewDate)}).`));
41
41
  keyCert = undefined;
42
42
  }
43
+ let timeUntilRenew = renewDate - Date.now();
44
+ setTimeout(() => {
45
+ console.log(magenta(`Clearing getHTTPSKeyCert to try to renew ${domain} (renew target is ${formatDateTime(renewDate)}).`));
46
+ getHTTPSKeyCert.clear(domain);
47
+ void getHTTPSKeyCert(domain);
48
+ }, Math.min(Math.floor(timeUntilRenew * 1.1), 2 ** 30));
43
49
  } else {
44
50
  console.log(magenta(`No cert found for domain ${domain}, generating shortly.`));
45
51
  }
@@ -338,7 +338,7 @@ export class ServiceDetailPage extends qreact.Component {
338
338
 
339
339
  <ATag values={[
340
340
  managementPageURL.getOverride("LogViewer2"),
341
- filterParam.getOverride(`__nodeId = ${machineId}`),
341
+ filterParam.getOverride(`__machineId = ${machineId}`),
342
342
  ]}>
343
343
  Machine Logs
344
344
  </ATag>
@@ -171,7 +171,9 @@ async function main() {
171
171
  // Get RAM amount to calculate appropriate swap size
172
172
  const memInfo = await runPromise(`ssh ${sshRemote} "free -m | grep Mem"`);
173
173
  const ramMB = parseInt(memInfo.split(/\s+/)[1]);
174
- if (swapTotal > 0) {
174
+ if (Number.isNaN(swapTotal) || Number.isNaN(ramMB)) {
175
+ console.error(`Error getting swap or memory info, swapInfo: ${swapInfo}, memInfo: ${memInfo}`);
176
+ } else if (swapTotal > 0) {
175
177
  console.log(`✅ Swap already configured: ${swapTotal}MB SWAP vs ${ramMB}MB REAL MEMORY`);
176
178
  } else {
177
179
  // Calculate swap size based on RAM:
@@ -213,6 +215,11 @@ async function main() {
213
215
 
214
216
  console.log("Setting up machine:", sshRemote);
215
217
 
218
+ // 0. Update apt
219
+ console.log("Updating apt...");
220
+ await runPromise(`ssh ${sshRemote} "sudo apt update"`);
221
+ console.log("✅ Apt updated");
222
+
216
223
  // 1. Copy backblaze file to remote server (~/backblaze.json)
217
224
  console.log("Copying backblaze credentials...");
218
225
  if (fs.existsSync(backblazePath)) {
@@ -23,7 +23,6 @@ import { ValueAuditController } from "../../src/5-diagnostics/memoryValueAudit";
23
23
  import { getExternalIP } from "../../src/misc/networking";
24
24
  import dns from "dns";
25
25
  import { getNodeIdDomain, getNodeIdIP } from "socket-function/src/nodeCache";
26
- import { debugGetRawEdgeCert } from "../../src/-e-certs/EdgeCertController";
27
26
  import ws from "ws";
28
27
  import https from "https";
29
28
  import debugbreak from "debugbreak";
@@ -38,6 +37,7 @@ import { ATag } from "../library-components/ATag";
38
37
  import { getSyncedController } from "../library-components/SyncedController";
39
38
  import child_process from "child_process";
40
39
  import { filterParam } from "./logs/FastArchiveViewer";
40
+ import { getHTTPSKeyCert } from "../-e-certs/certAuthority";
41
41
 
42
42
 
43
43
  type NodeData = {
@@ -431,7 +431,7 @@ class NodeViewerControllerBase {
431
431
  });
432
432
 
433
433
  let baseNodeIP = getNodeIdIP(nodeId);
434
- let cert = await debugGetRawEdgeCert();
434
+ let cert = await getHTTPSKeyCert(getDomain());
435
435
 
436
436
  const child = child_process.spawn("node", [
437
437
  "-e",
@@ -110,6 +110,14 @@ export function getFileTimeStamp(path: string): number {
110
110
  return new Date(dateStr).getTime();
111
111
  }
112
112
 
113
+
114
+ // NOTE: Global, to prevent hot reloading, or redundant FastArchiveAppendable, from breaking things.
115
+ const appendableSerialLock = (globalThis as any).appendableSerialLock ?? runInSerial(async (fnc: () => Promise<void>) => {
116
+ await fnc();
117
+ });
118
+ (globalThis as any).appendableSerialLock = appendableSerialLock;
119
+
120
+
113
121
  export class FastArchiveAppendable<Datum> {
114
122
  private lastSizeWarningTime = 0;
115
123
 
@@ -181,14 +189,10 @@ export class FastArchiveAppendable<Datum> {
181
189
  }
182
190
  }
183
191
 
184
- private serialLock = runInSerial(async (fnc: () => Promise<void>) => {
185
- await fnc();
186
- });
187
-
188
192
  // NOTE: This is disk writing, which should be fast, but if it's slow we might be able to remove the measureWrap (as technically spending 50% of our time writing to the disk is fine, and won't lag anything).
189
193
  @measureFnc
190
194
  public async flushNow(now = Date.now()) {
191
- await this.serialLock(async () => {
195
+ await appendableSerialLock(async () => {
192
196
  // 2025-09-06T07
193
197
  let hourFile = new Date(now).toISOString().slice(0, 13) + ".log";
194
198
  let localCacheFolder = this.getLocalPathRoot() + getOwnThreadId() + "/";
@@ -259,13 +263,15 @@ export class FastArchiveAppendable<Datum> {
259
263
 
260
264
  public static getBackblazePath(config: { fileName: string; threadId: string }): string {
261
265
  // 2025-09-06T07
262
- let [year, month, day, hour] = config.fileName.split(/[-T:]/);
266
+ let mainName = config.fileName.replace(/\.log$/, "");
267
+ let [year, month, day, hour] = mainName.split(/[-T:]/);
263
268
  return `${year}/${month}/${day}/${hour}/${config.threadId}.log`;
264
269
  }
265
270
 
266
271
  public async moveLogsToBackblaze() {
267
- await this.serialLock(async () => {
272
+ await appendableSerialLock(async () => {
268
273
  let rootCacheFolder = this.getLocalPathRoot();
274
+ console.log(magenta(`Moving old logs to Backblaze from ${rootCacheFolder}`));
269
275
 
270
276
  let archives = this.getArchives();
271
277
  async function moveLogsForFolder(threadId: string) {
@@ -274,22 +280,25 @@ export class FastArchiveAppendable<Datum> {
274
280
  let files = await fs.promises.readdir(threadDir);
275
281
  for (let file of files) {
276
282
  if (file === "heartbeat") continue;
277
- // 2025-09-06T07
278
283
  let fullPath = threadDir + file;
279
-
280
- // We could use modified time here? Although, this is nice if we move files around, and then manually have them moved, although even then... this could cause problem be tripping while we are copying the file, so... maybe this is just wrong?
281
- let timeStamp = getFileTimeStamp(fullPath);
282
- if (timeStamp > Date.now() - UPLOAD_THRESHOLD) continue;
283
-
284
-
285
- // NOTE: Because we use the same target path, if multiple services do this at the same time it's fine. Not great, but... fine.
286
- let backblazePath = FastArchiveAppendable.getBackblazePath({ fileName: file, threadId });
287
- console.log(magenta(`Moving ${fullPath} to Backblaze as ${backblazePath}`));
288
- let data = await measureBlock(async () => fs.promises.readFile(fullPath), "FastArchiveAppendable|readBeforeUploading");
289
- let compressed = await measureBlock(async () => Zip.gzip(data), "FastArchiveAppendable|compress");
290
- console.log(`Uploading ${formatNumber(data.length)}B (compressed to ${formatNumber(compressed.length)}B) logs to ${backblazePath} from ${fullPath}`);
291
- await archives.set(backblazePath, compressed);
292
- await fs.promises.unlink(fullPath);
284
+ try {
285
+ // We could use modified time here? Although, this is nice if we move files around, and then manually have them moved, although even then... this could cause problem be tripping while we are copying the file, so... maybe this is just wrong?
286
+ let timeStamp = getFileTimeStamp(fullPath);
287
+ if (timeStamp > Date.now() - UPLOAD_THRESHOLD) continue;
288
+
289
+ // NOTE: Because we use the same target path, if multiple services do this at the same time it's fine. Not great, but... fine.
290
+ let backblazePath = FastArchiveAppendable.getBackblazePath({ fileName: file, threadId });
291
+ console.log(magenta(`Moving ${fullPath} to Backblaze as ${backblazePath}`));
292
+
293
+ let data = await measureBlock(async () => fs.promises.readFile(fullPath), "FastArchiveAppendable|readBeforeUploading");
294
+ let compressed = await measureBlock(async () => Zip.gzip(data), "FastArchiveAppendable|compress");
295
+ console.log(`Uploading ${formatNumber(data.length)}B (compressed to ${formatNumber(compressed.length)}B) logs to ${backblazePath} from ${fullPath}`);
296
+ await archives.set(backblazePath, compressed);
297
+ await fs.promises.unlink(fullPath);
298
+ } catch (e: any) {
299
+ // Just skip it, if the first file in the directory is broken we don't want to never move any files
300
+ console.error(`Error moving log file ${fullPath}: ${e.stack}`);
301
+ }
293
302
  }
294
303
  }
295
304
 
@@ -553,7 +553,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
553
553
  </div>
554
554
  </div>
555
555
 
556
- <div className={css.vbox(12).fillWidth}>
556
+ <div className={css.vbox(12).fillWidth.paddingBottom(20)}>
557
557
  <InputLabelURL
558
558
  label={
559
559
  <div className={css.hbox(10)}>
@@ -179,7 +179,6 @@ export class ErrorWarning extends qreact.Component {
179
179
  }
180
180
  }}
181
181
  className={css.fontSize(12).pad2(4, 6)}
182
- disabled={!this.state.suppressionInput.trim()}
183
182
  >
184
183
  Fixed
185
184
  </Button>
@@ -196,7 +195,6 @@ export class ErrorWarning extends qreact.Component {
196
195
  }
197
196
  }}
198
197
  className={css.fontSize(12).pad2(4, 6)}
199
- disabled={!this.state.suppressionInput.trim()}
200
198
  >
201
199
  Not a bug
202
200
  </Button>
@@ -6,7 +6,9 @@ Very small amount of data
6
6
 
7
7
 
8
8
  4) Setup new hetnzer server
9
- yarn setup-machine 176.9.2.136
9
+
10
+ yarn setup-machine 176.9.2.136
11
+
10
12
  5) Update all services, and move them to that machine
11
13
  5) Verify the hezner server can run the site well
12
14
  6) Take down our digital ocean server
@@ -19,6 +21,7 @@ Very small amount of data
19
21
  - Reset filter in FastArchiveViewer
20
22
  - First observe the overlap with it and the BookOverview
21
23
  - If we are actually on the book overview page and we close the management page then that shouldn't reset it. We just want to reset it when you change pages. Because we want you to be able to hide and show the management page quickly if you want to double check something. Generally speaking though, you won't be on a page with filter and then going back and forth. And if you are, whatever. That's just the management page. We just want to avoid the overall confusion and annoyance of having lots of pre-filled values (And mostly the confusion of having filter prefilled all the time because it's always going to be set because everyone uses it and no one resets it at the moment.
24
+ - DON'T reset tab. It's useful to remember the tab? Hmm... sometimes at least...
22
25
 
23
26
 
24
27