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.
@@ -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, 60).pad2(10).pointerEvents("all")}>
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
- + " ManagementRoot-page"
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/errorNotifications";
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; }) {