meta-sovereign 0.49.7 → 0.49.8
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/js/src/web/api-errors.js +163 -0
- package/js/src/web/app.css +25 -0
- package/js/src/web/app.js +4 -0
- package/js/src/web/app.min.js +139 -62
- package/js/src/web/automations-state.js +8 -2
- package/js/src/web/automations-view.js +14 -3
- package/js/src/web/chat-live.js +6 -1
- package/js/src/web/chat-state.js +18 -4
- package/js/src/web/chat-view.js +12 -1
- package/js/src/web/connections/fetch-now.js +4 -1
- package/js/src/web/connections/index.js +21 -12
- package/js/src/web/connections/whatsapp-web.js +6 -1
- package/js/src/web/dialog-focus.js +115 -0
- package/js/src/web/dom.js +13 -0
- package/js/src/web/locales/en.js +26 -0
- package/js/src/web/locales/hi.js +26 -0
- package/js/src/web/locales/ru.js +26 -0
- package/js/src/web/locales/zh.js +21 -0
- package/js/src/web/views.js +12 -6
- package/package.json +4 -2
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// A server refusal never goes unseen (R-Z12). Every answer in the uniform
|
|
2
|
+
// `{ ok: false, error, code }` shape is logged to the console once per
|
|
3
|
+
// distinct error; a user-triggered (non-GET) request that fails with a
|
|
4
|
+
// systemic code also raises a localised toast, so a failed send or save is
|
|
5
|
+
// never a silent empty panel. Background reads stay quiet on the page.
|
|
6
|
+
|
|
7
|
+
import React, { useEffect, useState } from 'react';
|
|
8
|
+
import { useT } from './i18n.js';
|
|
9
|
+
|
|
10
|
+
const el = React.createElement;
|
|
11
|
+
|
|
12
|
+
export const API_ERROR_EVENT = 'ms-api-error';
|
|
13
|
+
|
|
14
|
+
// Codes with their own wording (error.code.<code> in every locale).
|
|
15
|
+
export const KNOWN_CODES = [
|
|
16
|
+
'bad-host',
|
|
17
|
+
'too-large',
|
|
18
|
+
'bad-json',
|
|
19
|
+
'internal',
|
|
20
|
+
'send-unknown',
|
|
21
|
+
'unsupported',
|
|
22
|
+
'unsupported-chat',
|
|
23
|
+
'captcha',
|
|
24
|
+
'LOW_DISK',
|
|
25
|
+
'STORE_WRITE_FAILED',
|
|
26
|
+
'STORE_FULL',
|
|
27
|
+
'STORE_LOCKED',
|
|
28
|
+
'outbox-unavailable',
|
|
29
|
+
'send-in-doubt',
|
|
30
|
+
'timeout',
|
|
31
|
+
'network',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Never a toast: a feature probe or a chat that cannot do it is explained
|
|
35
|
+
// inline by the panel that asked. Codes outside KNOWN_CODES are the panels'.
|
|
36
|
+
const INLINE_ONLY = new Set(['unsupported', 'unsupported-chat']);
|
|
37
|
+
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
|
|
40
|
+
/** The localised sentence for a server code, else the server's own text. */
|
|
41
|
+
export const errorText = (t, code, fallback = '') => {
|
|
42
|
+
const key = `error.code.${code}`;
|
|
43
|
+
const text = code ? t(key) : key;
|
|
44
|
+
return text === key ? fallback || t('error.code.unknown') : text;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** The `{ code, error }` of a failed answer, or null when it succeeded. */
|
|
48
|
+
export const failureOf = (status, data) => {
|
|
49
|
+
if (
|
|
50
|
+
data?.ok === false ||
|
|
51
|
+
(typeof data?.error === 'string' && status >= 400)
|
|
52
|
+
) {
|
|
53
|
+
return { code: data.code ?? '', error: String(data.error ?? '') };
|
|
54
|
+
}
|
|
55
|
+
return status >= 500 ? { code: 'internal', error: '' } : null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Logs once per distinct error; raises the toast for user-triggered calls. */
|
|
59
|
+
export const reportApiFailure = ({ path, method = 'GET', status, data }) => {
|
|
60
|
+
const failure = failureOf(status, data);
|
|
61
|
+
if (!failure) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const key = `${method} ${path} ${failure.code} ${failure.error}`;
|
|
65
|
+
if (!seen.has(key)) {
|
|
66
|
+
seen.add(key);
|
|
67
|
+
globalThis.console?.warn?.(
|
|
68
|
+
`[meta-sovereign] ${method} ${path} failed (${status}): ${failure.code} ${failure.error}`.trim()
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (
|
|
72
|
+
method !== 'GET' &&
|
|
73
|
+
KNOWN_CODES.includes(failure.code) &&
|
|
74
|
+
!INLINE_ONLY.has(failure.code)
|
|
75
|
+
) {
|
|
76
|
+
globalThis.dispatchEvent?.(
|
|
77
|
+
new CustomEvent(API_ERROR_EVENT, { detail: { ...failure, path } })
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return failure;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Forgets what was logged (tests). */
|
|
84
|
+
export const resetApiErrors = () => seen.clear();
|
|
85
|
+
|
|
86
|
+
/** The toast, mounted once in the app; Escape or × hides it. */
|
|
87
|
+
export const ApiErrorToast = () => {
|
|
88
|
+
const t = useT();
|
|
89
|
+
const [shown, setShown] = useState(null);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const on = (event) => setShown(event.detail);
|
|
92
|
+
globalThis.addEventListener?.(API_ERROR_EVENT, on);
|
|
93
|
+
return () => globalThis.removeEventListener?.(API_ERROR_EVENT, on);
|
|
94
|
+
}, []);
|
|
95
|
+
if (!shown) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return el(
|
|
99
|
+
'aside',
|
|
100
|
+
{
|
|
101
|
+
className: 'disk-banner api-error-toast',
|
|
102
|
+
role: 'alert',
|
|
103
|
+
'data-banner': 'api-error',
|
|
104
|
+
'data-error-code': shown.code,
|
|
105
|
+
},
|
|
106
|
+
[
|
|
107
|
+
el('span', { key: 'text' }, errorText(t, shown.code, shown.error)),
|
|
108
|
+
el(
|
|
109
|
+
'button',
|
|
110
|
+
{
|
|
111
|
+
key: 'x',
|
|
112
|
+
type: 'button',
|
|
113
|
+
'data-action': 'api-error-dismiss',
|
|
114
|
+
onClick: () => setShown(null),
|
|
115
|
+
},
|
|
116
|
+
t('disk.dismiss')
|
|
117
|
+
),
|
|
118
|
+
]
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** Logs a background failure to the console once per distinct message. */
|
|
123
|
+
export const logOnce = (what, error) => {
|
|
124
|
+
const key = `${what} ${String(error?.message ?? error)}`;
|
|
125
|
+
if (!seen.has(key)) {
|
|
126
|
+
seen.add(key);
|
|
127
|
+
globalThis.console?.warn?.(`[meta-sovereign] ${key}`);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** A list that could not load: what failed, and a button to try again. */
|
|
132
|
+
export const LoadError = ({ error, retry, what }) => {
|
|
133
|
+
const t = useT();
|
|
134
|
+
const code = typeof error === 'object' ? error?.code : '';
|
|
135
|
+
return el(
|
|
136
|
+
'div',
|
|
137
|
+
{
|
|
138
|
+
className: 'load-error',
|
|
139
|
+
role: 'alert',
|
|
140
|
+
'data-load-error': what ?? 'list',
|
|
141
|
+
},
|
|
142
|
+
[
|
|
143
|
+
el('p', { key: 'm' }, t('error.load')),
|
|
144
|
+
el(
|
|
145
|
+
'p',
|
|
146
|
+
{ key: 'd', className: 'meta' },
|
|
147
|
+
errorText(t, code, String(error?.message ?? error ?? ''))
|
|
148
|
+
),
|
|
149
|
+
retry
|
|
150
|
+
? el(
|
|
151
|
+
'button',
|
|
152
|
+
{
|
|
153
|
+
key: 'r',
|
|
154
|
+
type: 'button',
|
|
155
|
+
'data-action': 'load-retry',
|
|
156
|
+
onClick: retry,
|
|
157
|
+
},
|
|
158
|
+
t('error.retry')
|
|
159
|
+
)
|
|
160
|
+
: null,
|
|
161
|
+
]
|
|
162
|
+
);
|
|
163
|
+
};
|
package/js/src/web/app.css
CHANGED
|
@@ -12531,3 +12531,28 @@ a.remote-chip:hover {
|
|
|
12531
12531
|
text-transform: none;
|
|
12532
12532
|
text-decoration: underline;
|
|
12533
12533
|
}
|
|
12534
|
+
|
|
12535
|
+
/* A list that could not load, with a retry button (R-Z12). */
|
|
12536
|
+
.load-error {
|
|
12537
|
+
margin: 1rem;
|
|
12538
|
+
padding: 0.75rem 1rem;
|
|
12539
|
+
border: 1px solid var(--border);
|
|
12540
|
+
border-left: 4px solid var(--accent);
|
|
12541
|
+
border-radius: 8px;
|
|
12542
|
+
background: var(--bg-elev);
|
|
12543
|
+
}
|
|
12544
|
+
|
|
12545
|
+
/* The error toast floats above any open dialog so a refusal is never hidden. */
|
|
12546
|
+
.api-error-toast {
|
|
12547
|
+
position: fixed;
|
|
12548
|
+
top: 0.75rem;
|
|
12549
|
+
left: 50%;
|
|
12550
|
+
transform: translateX(-50%);
|
|
12551
|
+
z-index: 10001;
|
|
12552
|
+
max-width: min(36rem, calc(100vw - 2rem));
|
|
12553
|
+
margin: 0;
|
|
12554
|
+
box-shadow: 0 4px 16px rgb(0 0 0 / 25%);
|
|
12555
|
+
}
|
|
12556
|
+
main[data-view='chat'] > .api-error-toast {
|
|
12557
|
+
margin: 0;
|
|
12558
|
+
}
|
package/js/src/web/app.js
CHANGED
|
@@ -26,6 +26,8 @@ import { useAppearance, useApplyAppearance } from './appearance.js';
|
|
|
26
26
|
import { WebModeBanner } from './web-mode-banner.js';
|
|
27
27
|
import { DiskBanner } from './disk-banner.js';
|
|
28
28
|
import { SchemaBanner } from './schema-banner.js';
|
|
29
|
+
import { ApiErrorToast } from './api-errors.js';
|
|
30
|
+
import { installDialogFocus } from './dialog-focus.js';
|
|
29
31
|
import { shouldAutoOpen, useAnyConnection } from './tutorial-start.js';
|
|
30
32
|
import { Icon } from './icons.js';
|
|
31
33
|
import { ModeBadge } from './telegram-online.js';
|
|
@@ -207,6 +209,7 @@ const App = () => {
|
|
|
207
209
|
[
|
|
208
210
|
el(DiskBanner, { key: 'disk', api }),
|
|
209
211
|
el(SchemaBanner, { key: 'schema', api }),
|
|
212
|
+
el(ApiErrorToast, { key: 'api-error' }),
|
|
210
213
|
el(WebModeBanner, {
|
|
211
214
|
key: 'web-mode',
|
|
212
215
|
online,
|
|
@@ -257,4 +260,5 @@ const Root = () => {
|
|
|
257
260
|
|
|
258
261
|
const mount = document.getElementById('app');
|
|
259
262
|
installClientLog({ send: api.clientLog });
|
|
263
|
+
installDialogFocus();
|
|
260
264
|
createRoot(mount).render(el(Root));
|