querysub 0.515.0 → 0.517.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 +1 -1
- package/src/4-deploy/edgeBootstrap.ts +9 -0
- package/src/deployManager/components/ServicesListPage.tsx +28 -4
- package/src/deployManager/components/deployButtons.tsx +32 -1
- package/src/diagnostics/logs/errorNotifications2/ErrorNotificationPage.tsx +1 -1
- package/src/diagnostics/logs/errorNotifications2/ErrorWarning.tsx +16 -3
- package/src/diagnostics/managementPages.tsx +18 -17
- package/src/library-components/EdgeNodeSelector.tsx +10 -1
package/package.json
CHANGED
|
@@ -475,6 +475,15 @@ async function edgeNodeFunction(config: {
|
|
|
475
475
|
// The url override wins over everything (including the IP domain default). If the host isn't in the list, it is ignored.
|
|
476
476
|
{
|
|
477
477
|
let overrideHost = new URL(document.location.href).searchParams.get("edgenode");
|
|
478
|
+
if (overrideHost) {
|
|
479
|
+
// The URLParam system rewrites search params JSON encoded (strings get quotation marks), so decode the same way it does
|
|
480
|
+
try {
|
|
481
|
+
let parsed = JSON.parse(overrideHost);
|
|
482
|
+
if (typeof parsed === "string") {
|
|
483
|
+
overrideHost = parsed;
|
|
484
|
+
}
|
|
485
|
+
} catch { }
|
|
486
|
+
}
|
|
478
487
|
if (overrideHost) {
|
|
479
488
|
let overrideNode = edgeNodes.find(x => x.host === overrideHost);
|
|
480
489
|
if (overrideNode) {
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
2
|
import { qreact } from "../../4-dom/qreact";
|
|
3
|
-
import { DEFAULT_OVERLAP_TIME, MachineServiceController, ServiceConfig } from "../machineSchema";
|
|
3
|
+
import { DEFAULT_OVERLAP_TIME, MachineServiceController, ServiceConfig, getLiveServiceParameters } from "../machineSchema";
|
|
4
4
|
import { css } from "typesafecss";
|
|
5
5
|
import { t } from "../../2-proxy/schema2";
|
|
6
6
|
import { Querysub } from "../../4-querysub/Querysub";
|
|
7
7
|
import { currentViewParam, selectedServiceIdParam } from "../urlParams";
|
|
8
|
-
import { formatNiceDateTime, formatTime, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
9
|
-
import { sort, timeInMinute } from "socket-function/src/misc";
|
|
8
|
+
import { formatDateTimeDetailed, formatNiceDateTime, formatTime, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
9
|
+
import { sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
10
10
|
import { cache } from "socket-function/src/caching";
|
|
11
11
|
import { Anchor } from "../../library-components/ATag";
|
|
12
12
|
import { isPublic } from "../../config";
|
|
13
|
-
import { UpdateButtons, UpdateServiceButtons } from "./deployButtons";
|
|
13
|
+
import { PendingDeployInfo, UpdateButtons, UpdateServiceButtons } from "./deployButtons";
|
|
14
14
|
import { isDefined } from "../../misc";
|
|
15
15
|
import { formatDateJSX } from "../../misc/formatJSX";
|
|
16
16
|
import { Tools } from "./Tools";
|
|
@@ -26,6 +26,8 @@ export class ServicesListPage extends qreact.Component {
|
|
|
26
26
|
|
|
27
27
|
if (!serviceList) return <div>Loading services...</div>;
|
|
28
28
|
|
|
29
|
+
let now = Querysub.nowDelayed(timeInSecond);
|
|
30
|
+
|
|
29
31
|
let services = serviceList.map(serviceId => [serviceId, controller.getServiceConfig(serviceId)] as const);
|
|
30
32
|
sort(services, x => -(x[1]?.info.lastUpdatedTime || Date.now()) - (x[1]?.parameters.deploy && (Number.MAX_SAFE_INTEGER / 2) || 0));
|
|
31
33
|
|
|
@@ -140,10 +142,32 @@ export class ServicesListPage extends qreact.Component {
|
|
|
140
142
|
<div className={css.vbox(4)}>
|
|
141
143
|
<div>Updated {formatDateJSX(config.info.lastUpdatedTime)} AGO</div>
|
|
142
144
|
{config.parameters.overlapTime && <div className={css.colorhsl(210, 50, 50).fontSize(14).fontWeight("bold")}>Deploy overlap: {formatTime(config.parameters.overlapTime)}</div> || undefined}
|
|
145
|
+
{(() => {
|
|
146
|
+
let releaseTime = config.parameters.releaseTime || 0;
|
|
147
|
+
if (!releaseTime || releaseTime <= now || !config.oldParameters) return undefined;
|
|
148
|
+
let live = getLiveServiceParameters(config) as Record<string, unknown>;
|
|
149
|
+
let next = config.parameters as Record<string, unknown>;
|
|
150
|
+
let diffLines: string[] = [];
|
|
151
|
+
for (let key of new Set([...Object.keys(live), ...Object.keys(next)])) {
|
|
152
|
+
if (key === "releaseTime") continue;
|
|
153
|
+
let oldValue = JSON.stringify(live[key]);
|
|
154
|
+
let newValue = JSON.stringify(next[key]);
|
|
155
|
+
if (oldValue !== newValue) {
|
|
156
|
+
diffLines.push(`${key}: ${oldValue} -> ${newValue}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return <div
|
|
160
|
+
className={css.colorhsl(35, 80, 45).fontSize(14).fontWeight("bold")}
|
|
161
|
+
title={`Deploys at ${formatDateTimeDetailed(releaseTime)}\n\n${diffLines.join("\n")}`}
|
|
162
|
+
>
|
|
163
|
+
🕐 Deploys in {formatTime(releaseTime - now)}
|
|
164
|
+
</div>;
|
|
165
|
+
})()}
|
|
143
166
|
</div>
|
|
144
167
|
</div>
|
|
145
168
|
</Anchor>
|
|
146
169
|
<UpdateServiceButtons service={config} />
|
|
170
|
+
<PendingDeployInfo service={config} />
|
|
147
171
|
</div>
|
|
148
172
|
;
|
|
149
173
|
})}
|
|
@@ -5,7 +5,8 @@ import { Querysub } from "../../4-querysub/Querysub";
|
|
|
5
5
|
import { DEFAULT_OVERLAP_TIME, MachineServiceController, MachineInfo, ServiceConfig } from "../machineSchema";
|
|
6
6
|
import { showFullscreenModal } from "../../5-diagnostics/FullscreenModal";
|
|
7
7
|
import { css } from "../../4-dom/css";
|
|
8
|
-
import { formatTime } from "socket-function/src/formatting/format";
|
|
8
|
+
import { formatDateTimeDetailed, formatTime } from "socket-function/src/formatting/format";
|
|
9
|
+
import { timeInSecond } from "socket-function/src/misc";
|
|
9
10
|
import { formatDateJSX } from "../../misc/formatJSX";
|
|
10
11
|
import { MachineController } from "../machineController";
|
|
11
12
|
import { deepCloneJSON } from "socket-function/src/misc";
|
|
@@ -192,6 +193,36 @@ export class UpdateServiceButtons extends qreact.Component<{
|
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
|
|
196
|
+
/** Shows a service's scheduled (not yet live) deploy, in the same style as the update buttons (but blue, and not clickable): when it goes live, and the new vs currently live commit. */
|
|
197
|
+
export class PendingDeployInfo extends qreact.Component<{
|
|
198
|
+
service: ServiceConfig;
|
|
199
|
+
}> {
|
|
200
|
+
render() {
|
|
201
|
+
let service = this.props.service;
|
|
202
|
+
let releaseTime = service.parameters.releaseTime || 0;
|
|
203
|
+
let now = Querysub.nowDelayed(timeInSecond);
|
|
204
|
+
const oldParameters = service.oldParameters;
|
|
205
|
+
if (!releaseTime || releaseTime <= now || !oldParameters) return undefined;
|
|
206
|
+
|
|
207
|
+
return <div
|
|
208
|
+
className={css.pad2(12, 8).bord2(210, 60, 40).fontWeight("bold").vbox(2).alignItems("center").alignSelf("stretch").hsl(210, 70, 90)}
|
|
209
|
+
title={formatDateTimeDetailed(releaseTime)}
|
|
210
|
+
>
|
|
211
|
+
<div>
|
|
212
|
+
{bigEmoji("🕐", -4)} <span>Deploys in {formatTime(releaseTime - now)}</span>
|
|
213
|
+
</div>
|
|
214
|
+
<div className={css.hbox(5)}>
|
|
215
|
+
<b>New</b>
|
|
216
|
+
<RenderGitRefInfo gitRef={service.parameters.gitRef} />
|
|
217
|
+
</div>
|
|
218
|
+
<div className={css.hbox(5)}>
|
|
219
|
+
<b>Current</b>
|
|
220
|
+
<RenderGitRefInfo gitRef={oldParameters.gitRef} />
|
|
221
|
+
</div>
|
|
222
|
+
</div>;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
195
226
|
export class DeployMachineButtons extends qreact.Component<{
|
|
196
227
|
machines: MachineInfo[];
|
|
197
228
|
}> {
|
|
@@ -4,8 +4,9 @@ import { css } from "typesafecss";
|
|
|
4
4
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
5
5
|
import { ATag } from "../../../library-components/ATag";
|
|
6
6
|
import { managementPageURL, showingManagementURL } from "../../managementPages";
|
|
7
|
-
import { LogDatumRenderer } from "./ErrorNotificationPage";
|
|
7
|
+
import { getMatcher, LogDatumRenderer } from "./ErrorNotificationPage";
|
|
8
8
|
import { getErrorNotificationsManager } from "./errorWatcher";
|
|
9
|
+
import { LogDatum } from "../diskLogger";
|
|
9
10
|
import { TicketsController } from "../errorTickets/tickets";
|
|
10
11
|
import { STATE_HUES, ticketFilterURL } from "../errorTickets/TicketPage";
|
|
11
12
|
import { isTicketFinished, TicketState, TICKET_STATES } from "../errorTickets/ticketTypes";
|
|
@@ -49,9 +50,21 @@ export class ErrorWarning extends qreact.Component {
|
|
|
49
50
|
render() {
|
|
50
51
|
let ticketCounts = this.renderTicketCounts();
|
|
51
52
|
|
|
53
|
+
// Errors that already have a ticket in an open (not finished) state are hidden here — there's no point telling the user about a bug that is already under investigation
|
|
54
|
+
let openTicketPatterns: string[] = [];
|
|
55
|
+
for (let ticket of TicketsController(SocketFunction.browserNodeId()).getTickets() || []) {
|
|
56
|
+
if (isTicketFinished(ticket.state)) continue;
|
|
57
|
+
if (!ticket.suppressionPattern) continue;
|
|
58
|
+
openTicketPatterns.push(ticket.suppressionPattern);
|
|
59
|
+
}
|
|
60
|
+
const isCoveredByOpenTicket = (datum: LogDatum) => {
|
|
61
|
+
let errorBuffer = Buffer.from(JSON.stringify(datum));
|
|
62
|
+
return openTicketPatterns.some(pattern => getMatcher(pattern)(errorBuffer));
|
|
63
|
+
};
|
|
64
|
+
|
|
52
65
|
let errorWarning: preact.ComponentChild = undefined;
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
let firstError = !this.manager.state.isLoading && this.manager.state.unmatchedErrors.find(x => !isCoveredByOpenTicket(x)) || undefined;
|
|
67
|
+
if (firstError) {
|
|
55
68
|
errorWarning = <div className={css.hbox(4).alignItems("start").hsl(0, 0, 90).bord2(0, 0, 85).pad2(4, 2).hslcolor(0, 0, 0)}>
|
|
56
69
|
<ATag className={css.paddingTop(5).flexShrink0} values={[
|
|
57
70
|
showingManagementURL.getOverride(true),
|
|
@@ -80,6 +80,23 @@ export async function registerManagementPages2(config: {
|
|
|
80
80
|
let inputPages: typeof config.pages = [];
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
inputPages.push({
|
|
84
|
+
title: "Routing Table",
|
|
85
|
+
componentName: "AuthoritySpecPage",
|
|
86
|
+
controllerName: "AuthoritySpecPageController",
|
|
87
|
+
getModule: () => import("./misc-pages/AuthoritySpecPage"),
|
|
88
|
+
});
|
|
89
|
+
inputPages.push({
|
|
90
|
+
title: "LOG VIEWER",
|
|
91
|
+
componentName: "LogViewer3",
|
|
92
|
+
getModule: () => import("./logs/IndexedLogs/LogViewer3"),
|
|
93
|
+
});
|
|
94
|
+
inputPages.push({
|
|
95
|
+
title: "Tickets",
|
|
96
|
+
componentName: "TicketPage",
|
|
97
|
+
controllerName: "",
|
|
98
|
+
getModule: () => import("./logs/errorTickets/TicketPage"),
|
|
99
|
+
});
|
|
83
100
|
// IMPORTANT! The controller name has to be the name of the controller in the file. No human would just randomly put some junk there, but the AI keeps putting random junk there and being surprised when they get an error because that controller doesn't actually exist in that file.
|
|
84
101
|
inputPages.push({
|
|
85
102
|
title: "Machines",
|
|
@@ -111,12 +128,6 @@ export async function registerManagementPages2(config: {
|
|
|
111
128
|
controllerName: "SnapshotViewerController",
|
|
112
129
|
getModule: () => import("./misc-pages/SnapshotViewer"),
|
|
113
130
|
});
|
|
114
|
-
inputPages.push({
|
|
115
|
-
title: "Routing Table",
|
|
116
|
-
componentName: "AuthoritySpecPage",
|
|
117
|
-
controllerName: "AuthoritySpecPageController",
|
|
118
|
-
getModule: () => import("./misc-pages/AuthoritySpecPage"),
|
|
119
|
-
});
|
|
120
131
|
inputPages.push({
|
|
121
132
|
title: "DNS",
|
|
122
133
|
componentName: "DNSPage",
|
|
@@ -134,11 +145,6 @@ export async function registerManagementPages2(config: {
|
|
|
134
145
|
componentName: "FunctionCaptureAnalyzePage",
|
|
135
146
|
getModule: () => import("./misc-pages/FunctionCaptureAnalyzePage"),
|
|
136
147
|
});
|
|
137
|
-
inputPages.push({
|
|
138
|
-
title: "LOG VIEWER",
|
|
139
|
-
componentName: "LogViewer3",
|
|
140
|
-
getModule: () => import("./logs/IndexedLogs/LogViewer3"),
|
|
141
|
-
});
|
|
142
148
|
// It's really sad to comment out the life cycles page, but I'm just never going to use it, it's just too slow and too annoying. It's easier to get the AI to search stuff.
|
|
143
149
|
// inputPages.push({
|
|
144
150
|
// title: "Life Cycles",
|
|
@@ -152,12 +158,7 @@ export async function registerManagementPages2(config: {
|
|
|
152
158
|
controllerName: "",
|
|
153
159
|
getModule: () => import("./logs/errorNotifications2/ErrorNotificationPage"),
|
|
154
160
|
});
|
|
155
|
-
|
|
156
|
-
title: "Tickets",
|
|
157
|
-
componentName: "TicketPage",
|
|
158
|
-
controllerName: "",
|
|
159
|
-
getModule: () => import("./logs/errorTickets/TicketPage"),
|
|
160
|
-
});
|
|
161
|
+
|
|
161
162
|
// NOTE: Even if we never use this, it is needed if a server or site is bootstrapping. However, they should probably include this in their own header dynamically as well.
|
|
162
163
|
inputPages.push({
|
|
163
164
|
title: "Security",
|
|
@@ -17,12 +17,21 @@ export class EdgeNodeSelector extends qreact.Component<{}> {
|
|
|
17
17
|
let stats = globalThis.EDGE_NODE_STATS;
|
|
18
18
|
const booted = (globalThis as any).BOOTED_EDGE_NODE as EdgeNodeConfig | undefined;
|
|
19
19
|
if (!booted) return undefined;
|
|
20
|
+
// The URLParam system rewrites search params JSON encoded (strings get quotation marks), so read/write the same way it does
|
|
20
21
|
let forced = new URL(document.location.href).searchParams.get(EDGE_NODE_URL_PARAM) || "";
|
|
22
|
+
if (forced) {
|
|
23
|
+
try {
|
|
24
|
+
let parsed = JSON.parse(forced);
|
|
25
|
+
if (typeof parsed === "string") {
|
|
26
|
+
forced = parsed;
|
|
27
|
+
}
|
|
28
|
+
} catch { }
|
|
29
|
+
}
|
|
21
30
|
|
|
22
31
|
const setEdgeNode = (host: string) => {
|
|
23
32
|
let url = new URL(document.location.href);
|
|
24
33
|
if (host) {
|
|
25
|
-
url.searchParams.set(EDGE_NODE_URL_PARAM, host);
|
|
34
|
+
url.searchParams.set(EDGE_NODE_URL_PARAM, JSON.stringify(host));
|
|
26
35
|
} else {
|
|
27
36
|
url.searchParams.delete(EDGE_NODE_URL_PARAM);
|
|
28
37
|
}
|