querysub 0.378.0 → 0.379.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/.cursorrules +2 -0
- package/package.json +1 -1
- package/src/diagnostics/MachineThreadInfo.tsx +33 -10
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +5 -1
- package/src/diagnostics/logs/errorNotifications2/ErrorNotificationPage.tsx +430 -201
- package/src/diagnostics/logs/errorNotifications2/ErrorWarning.tsx +32 -0
- package/src/diagnostics/logs/errorNotifications2/errorNotifications.ts +414 -54
- package/src/diagnostics/logs/errorNotifications2/errorWatcher.ts +168 -0
- package/src/diagnostics/logs/errorNotifications2/openRouterHelper.ts +77 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +1 -30
- package/src/diagnostics/managementPages.tsx +6 -2
- package/src/user-implementation/SecurityPage.tsx +6 -0
- package/src/user-implementation/userData.ts +6 -1
- /package/src/library-components/{errorNotifications.tsx → uncaughtToast.tsx} +0 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
|
+
import { lazy } from "socket-function/src/caching";
|
|
3
|
+
import { nextId } from "socket-function/src/misc";
|
|
4
|
+
import { t } from "../../../2-proxy/schema2";
|
|
5
|
+
import { Querysub } from "../../../4-querysub/QuerysubController";
|
|
6
|
+
import { LogDatum } from "../diskLogger";
|
|
7
|
+
import { SuppressionMatch, watchUnmatchedErrors, ErrorNotificationsController } from "./errorNotifications";
|
|
8
|
+
|
|
9
|
+
let errorNotificationsLocalSchema = Querysub.createLocalSchema("errorNotifications", {
|
|
10
|
+
unmatchedErrors: t.atomic<LogDatum[]>([]),
|
|
11
|
+
suppressionMatches: t.atomic<Map<string, SuppressionMatch>>(new Map()),
|
|
12
|
+
suppressionEntries: t.atomic<Array<{
|
|
13
|
+
id: string;
|
|
14
|
+
notes?: string;
|
|
15
|
+
pattern: string;
|
|
16
|
+
timeout: number;
|
|
17
|
+
createdTime: number;
|
|
18
|
+
lastUpdatedTime: number;
|
|
19
|
+
}>>([]),
|
|
20
|
+
isLoading: t.atomic<boolean>(true),
|
|
21
|
+
initError: t.atomic<string | undefined>(undefined),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export let getErrorNotificationsManager = lazy(() => {
|
|
25
|
+
let state = errorNotificationsLocalSchema();
|
|
26
|
+
|
|
27
|
+
void (async () => {
|
|
28
|
+
try {
|
|
29
|
+
await watchUnmatchedErrors({
|
|
30
|
+
errorCallback: (datums: LogDatum[]) => {
|
|
31
|
+
Querysub.commit(() => {
|
|
32
|
+
state.unmatchedErrors = [...state.unmatchedErrors, ...datums];
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
suppressionCallback: (matches) => {
|
|
36
|
+
Querysub.commit(() => {
|
|
37
|
+
let newMatches = new Map(state.suppressionMatches);
|
|
38
|
+
let hasNewSuppression = false;
|
|
39
|
+
for (let match of matches) {
|
|
40
|
+
newMatches.set(match.id, match);
|
|
41
|
+
if (!state.suppressionEntries.find(e => e.id === match.id)) {
|
|
42
|
+
hasNewSuppression = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
state.suppressionMatches = newMatches;
|
|
46
|
+
if (hasNewSuppression) {
|
|
47
|
+
void reloadData();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
let controller = ErrorNotificationsController(SocketFunction.browserNodeId());
|
|
54
|
+
let data = await controller.getData.promise();
|
|
55
|
+
let suppressionEntries = await controller.getSuppressionEntries.promise();
|
|
56
|
+
|
|
57
|
+
Querysub.commit(() => {
|
|
58
|
+
state.unmatchedErrors = data.unmatchedErrors;
|
|
59
|
+
state.suppressionMatches = data.suppressionMatches;
|
|
60
|
+
state.suppressionEntries = suppressionEntries;
|
|
61
|
+
state.initError = undefined;
|
|
62
|
+
state.isLoading = false;
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
Querysub.commit(() => {
|
|
66
|
+
state.initError = error instanceof Error && error.message || String(error);
|
|
67
|
+
state.isLoading = false;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
|
|
72
|
+
async function reloadData() {
|
|
73
|
+
let controller = ErrorNotificationsController(SocketFunction.browserNodeId());
|
|
74
|
+
let data = await controller.getData.promise();
|
|
75
|
+
let suppressionEntries = await controller.getSuppressionEntries.promise();
|
|
76
|
+
Querysub.commit(() => {
|
|
77
|
+
state.unmatchedErrors = data.unmatchedErrors;
|
|
78
|
+
state.suppressionMatches = data.suppressionMatches;
|
|
79
|
+
state.suppressionEntries = suppressionEntries;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
state,
|
|
85
|
+
async addSuppression(pattern: string, timeout: number) {
|
|
86
|
+
if (!pattern) {
|
|
87
|
+
throw new Error("Pattern is required");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let now = Date.now();
|
|
91
|
+
let newEntry = {
|
|
92
|
+
id: nextId(),
|
|
93
|
+
pattern,
|
|
94
|
+
timeout,
|
|
95
|
+
createdTime: now,
|
|
96
|
+
lastUpdatedTime: now,
|
|
97
|
+
};
|
|
98
|
+
let controller = ErrorNotificationsController(SocketFunction.browserNodeId());
|
|
99
|
+
await controller.setSuppressionEntry.promise(newEntry);
|
|
100
|
+
await reloadData();
|
|
101
|
+
},
|
|
102
|
+
async updateSuppression(id: string, pattern: string) {
|
|
103
|
+
if (!pattern) {
|
|
104
|
+
throw new Error("Pattern is required");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const existingEntry = Querysub.fastRead(() => state.suppressionEntries.find((s: { id: string; }) => s.id === id));
|
|
108
|
+
if (!existingEntry) {
|
|
109
|
+
throw new Error(`Suppression with id ${id} not found`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let updatedEntry = {
|
|
113
|
+
id: existingEntry.id,
|
|
114
|
+
notes: existingEntry.notes,
|
|
115
|
+
pattern,
|
|
116
|
+
timeout: existingEntry.timeout,
|
|
117
|
+
createdTime: existingEntry.createdTime,
|
|
118
|
+
lastUpdatedTime: Date.now(),
|
|
119
|
+
};
|
|
120
|
+
let controller = ErrorNotificationsController(SocketFunction.browserNodeId());
|
|
121
|
+
await controller.setSuppressionEntry.promise(updatedEntry);
|
|
122
|
+
await reloadData();
|
|
123
|
+
},
|
|
124
|
+
async updateNotes(id: string, notes: string | undefined) {
|
|
125
|
+
const existingEntry = Querysub.fastRead(() => state.suppressionEntries.find((s: { id: string; }) => s.id === id));
|
|
126
|
+
if (!existingEntry) {
|
|
127
|
+
throw new Error(`Suppression with id ${id} not found`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
Querysub.commit(() => {
|
|
131
|
+
const idx = state.suppressionEntries.findIndex((s: { id: string; }) => s.id === id);
|
|
132
|
+
if (idx !== -1) {
|
|
133
|
+
let updated = [...state.suppressionEntries];
|
|
134
|
+
updated[idx] = { ...updated[idx], notes, lastUpdatedTime: Date.now() };
|
|
135
|
+
state.suppressionEntries = updated;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
let controller = ErrorNotificationsController(SocketFunction.browserNodeId());
|
|
140
|
+
await controller.updateSuppressionNotes.promise(id, notes);
|
|
141
|
+
},
|
|
142
|
+
async updateTimeout(id: string, timeout: number) {
|
|
143
|
+
const existingEntry = Querysub.fastRead(() => state.suppressionEntries.find((s: { id: string; }) => s.id === id));
|
|
144
|
+
if (!existingEntry) {
|
|
145
|
+
throw new Error(`Suppression with id ${id} not found`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let updatedEntry = {
|
|
149
|
+
...existingEntry,
|
|
150
|
+
timeout,
|
|
151
|
+
lastUpdatedTime: Date.now(),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
let controller = ErrorNotificationsController(SocketFunction.browserNodeId());
|
|
155
|
+
await controller.setSuppressionEntry.promise(updatedEntry);
|
|
156
|
+
await reloadData();
|
|
157
|
+
},
|
|
158
|
+
async deleteSuppression(id: string) {
|
|
159
|
+
let controller = ErrorNotificationsController(SocketFunction.browserNodeId());
|
|
160
|
+
await controller.deleteSuppressionEntry.promise(id);
|
|
161
|
+
await reloadData();
|
|
162
|
+
},
|
|
163
|
+
async logTestError(message: string) {
|
|
164
|
+
let controller = ErrorNotificationsController(SocketFunction.browserNodeId());
|
|
165
|
+
await controller.logTestError.promise(message);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export async function callOpenRouter(apiKey: string, prompt: string): Promise<string> {
|
|
2
|
+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
3
|
+
method: "POST",
|
|
4
|
+
headers: {
|
|
5
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
6
|
+
"Content-Type": "application/json",
|
|
7
|
+
},
|
|
8
|
+
body: JSON.stringify({
|
|
9
|
+
model: "google/gemini-3-flash-preview",
|
|
10
|
+
messages: [
|
|
11
|
+
{
|
|
12
|
+
role: "user",
|
|
13
|
+
content: prompt,
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
const errorText = await response.text();
|
|
21
|
+
throw new Error(`OpenRouter API error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const data = await response.json();
|
|
25
|
+
|
|
26
|
+
if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
|
|
27
|
+
throw new Error(`Unexpected OpenRouter API response format: ${JSON.stringify(data)}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return data.choices[0].message.content.trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function callOpenRouterJSON<T>(apiKey: string, prompt: string): Promise<T> {
|
|
34
|
+
const maxRetries = 3;
|
|
35
|
+
let lastError: Error | undefined;
|
|
36
|
+
|
|
37
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
model: "google/gemini-3-flash-preview",
|
|
47
|
+
messages: [
|
|
48
|
+
{
|
|
49
|
+
role: "user",
|
|
50
|
+
content: prompt,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
response_format: { type: "json_object" },
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const errorText = await response.text();
|
|
59
|
+
throw new Error(`OpenRouter API error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
|
|
64
|
+
if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) {
|
|
65
|
+
throw new Error(`Unexpected OpenRouter API response format: ${JSON.stringify(data)}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const content = data.choices[0].message.content.trim();
|
|
69
|
+
const parsed = JSON.parse(content) as T;
|
|
70
|
+
return parsed;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new Error(`Failed after ${maxRetries} attempts: ${lastError?.message}`);
|
|
77
|
+
}
|
|
@@ -22,41 +22,12 @@ IMPORTANT! Now I am properly calling shutdown, so none of the streamed logs shou
|
|
|
22
22
|
|
|
23
23
|
//todonext
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
1) Add button that will log an error serverside, to ensure this system works
|
|
27
|
-
- Allow typing any error we want
|
|
28
|
-
2) Write smaller component just for watching
|
|
29
|
-
- Lazily the component, and only if super user (just uncomment the code in Page.tsx)
|
|
30
|
-
|
|
31
|
-
3) Fix up the error watching UI
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
4) Publish
|
|
35
|
-
|
|
36
25
|
5) Deploy watcher service to remote (yarn error-watch-public)
|
|
26
|
+
6) Deploy CYOA
|
|
37
27
|
|
|
38
28
|
5) Test on actual server, with actual logs
|
|
39
29
|
|
|
40
30
|
|
|
41
|
-
// single service, no history search, discord messaging, suppression and suppression page
|
|
42
|
-
// - Storing suppression history just in memory? (although suppression values, obviously, on disk)
|
|
43
|
-
// - receive all errors, and service will do suppression
|
|
44
|
-
|
|
45
|
-
// Start with page itself showing errors and suppressing them, for debugging/testing
|
|
46
|
-
// - Ai to automatically create suppression searches, and combine existing ones, etc
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Rewrite error notification code
|
|
50
|
-
- Single service, which talks to all machines
|
|
51
|
-
- We'll have a dev mode, mostly for testing, although we won't usually run it.
|
|
52
|
-
- BROADCASTS messages to all http servers so they show them
|
|
53
|
-
- Broadcast messages only has one type now "error notification", but add TODO for more types, which may display in different ways (via toast, etc)
|
|
54
|
-
- Maybe only streaming? We'll miss some, but... not that much...
|
|
55
|
-
- ALSO owns the discord messaging code
|
|
56
|
-
- Suppression... will work in the service itself. Because there shouldn't be THAT many errors happening!
|
|
57
|
-
- Suppression will use AI to group?
|
|
58
|
-
- BUT, we will still have page that shows suppression list, with counts and times
|
|
59
|
-
- Page links to search, which tries to find those errors (we should use the same format for suppression as search, so it should be fairly easy to do that)
|
|
60
31
|
|
|
61
32
|
|
|
62
33
|
|
|
@@ -275,6 +275,8 @@ export function renderIsManagementUser() {
|
|
|
275
275
|
return isManagementUserValue().value;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
const ErrorWarning = createLazyComponent(() => import("./logs/errorNotifications2/ErrorWarning"))("ErrorWarning");
|
|
279
|
+
|
|
278
280
|
class ManagementRoot extends qreact.Component {
|
|
279
281
|
state = {
|
|
280
282
|
ready: false
|
|
@@ -319,6 +321,7 @@ class ManagementRoot extends qreact.Component {
|
|
|
319
321
|
.color("white")
|
|
320
322
|
.color("hsl(0, 0%, 7%)")
|
|
321
323
|
.pointerEvents("none")
|
|
324
|
+
+ " ManagementRoot-page"
|
|
322
325
|
}>
|
|
323
326
|
<style>
|
|
324
327
|
{`
|
|
@@ -327,7 +330,8 @@ class ManagementRoot extends qreact.Component {
|
|
|
327
330
|
}
|
|
328
331
|
`}
|
|
329
332
|
</style>
|
|
330
|
-
<div class={css.fillWidth.hbox(30, 10).wrap.hsl(245, 25,
|
|
333
|
+
<div class={css.fillWidth.hbox(30, 10).wrap.hsl(245, 25, 80).pad2(10).pointerEvents("all")}>
|
|
334
|
+
<ErrorWarning />
|
|
331
335
|
{pages.map(page =>
|
|
332
336
|
<ATag values={[{ param: managementPageURL, value: page.componentName }]}>{page.title}</ATag>
|
|
333
337
|
)}
|
|
@@ -337,7 +341,7 @@ class ManagementRoot extends qreact.Component {
|
|
|
337
341
|
class={
|
|
338
342
|
css.fillBoth.vbox0.overflowAuto.hsl(145, 50, 75)
|
|
339
343
|
.pointerEvents("all")
|
|
340
|
-
|
|
344
|
+
|
|
341
345
|
}
|
|
342
346
|
onKeyDown={e => {
|
|
343
347
|
if (e.currentTarget === e.target) {
|
|
@@ -59,6 +59,12 @@ class ConfigTab extends qreact.Component {
|
|
|
59
59
|
value={user_data().secure.notifyDiscordWebhookURL}
|
|
60
60
|
onChangeValue={value => user_functions.setNotifyDiscordWebhookURL({ webhookURL: value })}
|
|
61
61
|
/>
|
|
62
|
+
<InputLabel
|
|
63
|
+
label={"OpenRouter API Key"}
|
|
64
|
+
edit
|
|
65
|
+
value={user_data().secure.openRouterAPIKey}
|
|
66
|
+
onChangeValue={value => user_functions.setOpenRouterAPIKey({ apiKey: value })}
|
|
67
|
+
/>
|
|
62
68
|
<Button onClick={() => user_functions.testSendDiscordMessage()}>
|
|
63
69
|
Test Discord Message
|
|
64
70
|
</Button>
|
|
@@ -13,7 +13,7 @@ import { MAX_ACCEPTED_CHANGE_AGE } from "../0-path-value-core/pathValueCore";
|
|
|
13
13
|
import { createURLSync } from "../library-components/URLParam";
|
|
14
14
|
import { devDebugbreak, getDomain, getEmailDomain, isDevDebugbreak, isRecovery } from "../config";
|
|
15
15
|
import { delay } from "socket-function/src/batching";
|
|
16
|
-
import { enableErrorNotifications } from "../library-components/
|
|
16
|
+
import { enableErrorNotifications } from "../library-components/uncaughtToast";
|
|
17
17
|
import { clamp } from "../misc";
|
|
18
18
|
import { sha256 } from "js-sha256";
|
|
19
19
|
import { logDisk } from "../diagnostics/logs/diskLogger";
|
|
@@ -166,6 +166,7 @@ const { data, functions } = Querysub.syncSchema<{
|
|
|
166
166
|
signupsOpen?: boolean;
|
|
167
167
|
postmarkAPIKey?: string;
|
|
168
168
|
notifyDiscordWebhookURL?: string;
|
|
169
|
+
openRouterAPIKey?: string;
|
|
169
170
|
|
|
170
171
|
// NOTE: For now these will only be blocked at a user level. If we run into issues, we can
|
|
171
172
|
// make Querysub have configurable IP blocking that accepts a synced read function, which
|
|
@@ -223,6 +224,7 @@ const { data, functions } = Querysub.syncSchema<{
|
|
|
223
224
|
specialSetInviteCount,
|
|
224
225
|
setPostmarkAPIKey,
|
|
225
226
|
setNotifyDiscordWebhookURL,
|
|
227
|
+
setOpenRouterAPIKey,
|
|
226
228
|
specialSetUserType,
|
|
227
229
|
registerPageLoadTime,
|
|
228
230
|
banUser, unbanUser,
|
|
@@ -978,6 +980,9 @@ function setPostmarkAPIKey(config: { apiKey: string; }) {
|
|
|
978
980
|
function setNotifyDiscordWebhookURL(config: { webhookURL: string; }) {
|
|
979
981
|
data().secure.notifyDiscordWebhookURL = config.webhookURL;
|
|
980
982
|
}
|
|
983
|
+
function setOpenRouterAPIKey(config: { apiKey: string; }) {
|
|
984
|
+
data().secure.openRouterAPIKey = config.apiKey;
|
|
985
|
+
}
|
|
981
986
|
|
|
982
987
|
|
|
983
988
|
function banUser(config: { userId: string; }) {
|
|
File without changes
|