@tollbooth-dpyc/web 1.5.0 → 1.5.1
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/README.md +4 -0
- package/dist/react/AppShell.d.ts +10 -2
- package/dist/react/AppShell.js +28 -11
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +1 -0
- package/dist/react/statusCheck.d.ts +34 -0
- package/dist/react/statusCheck.js +39 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -227,6 +227,10 @@ function Nav() {
|
|
|
227
227
|
the lapsed-sign-in note), or the gate alone. The frame is a column: "page"
|
|
228
228
|
grows with the content; "viewport" pins it to the screen and lets the
|
|
229
229
|
content shrink, so the debug log's spacer never pushes a bottom rail off.
|
|
230
|
+
`shell.statusState` is `"connecting"`, `"ready"` or `"failed"`;
|
|
231
|
+
`shell.statusError` is the failure as one short, scrubbed line; and
|
|
232
|
+
`shell.retryStatus()` runs the check again — so a banner can say
|
|
233
|
+
"connection failed" rather than "Connecting…" through an outage.
|
|
230
234
|
- `SiteNav`: brand, pages (icon, label, badge), `trailing`, and an account
|
|
231
235
|
menu (avatar → identity, links, Log out). Below `collapse` (default
|
|
232
236
|
`(max-width: 639px)`; `false` never) the pages fold into a menu button.
|
package/dist/react/AppShell.d.ts
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* `signedOut` the gate is the page.
|
|
13
13
|
*
|
|
14
14
|
* Inside, `useAppShell()` gives any component the same `shell` — the npub,
|
|
15
|
-
* `signOut`, the `service_status` answer
|
|
16
|
-
*
|
|
15
|
+
* `signOut`, the `service_status` answer and whether that check is still
|
|
16
|
+
* connecting, answered or failed (with `retryStatus()` to ask again) — so a
|
|
17
|
+
* site keeps no session context of its own.
|
|
17
18
|
*
|
|
18
19
|
* Also done here, once: the stored theme applied before paint and followed
|
|
19
20
|
* across tabs; the npub's kind-0 picture seeded as its avatar; `DebugPanel`
|
|
@@ -24,11 +25,18 @@ import { type ServiceStatus } from "../standardTools.ts";
|
|
|
24
25
|
import { type Theme } from "../theme.ts";
|
|
25
26
|
import { type DebugPanelProps } from "./DebugPanel.tsx";
|
|
26
27
|
import { type ShellFit } from "./shellLayout.ts";
|
|
28
|
+
import { type StatusState } from "./statusCheck.ts";
|
|
27
29
|
import { type Session } from "./useSession.ts";
|
|
28
30
|
export interface AppShellContext {
|
|
29
31
|
session: Session;
|
|
30
32
|
/** The `service_status` answer, or null until (or unless) it comes. */
|
|
31
33
|
status: ServiceStatus | null;
|
|
34
|
+
/** Where the `service_status` check stands: "connecting", "ready" or "failed". */
|
|
35
|
+
statusState: StatusState;
|
|
36
|
+
/** Why the last check failed — short, human, scrubbed of secrets — or null. */
|
|
37
|
+
statusError: string | null;
|
|
38
|
+
/** Run the `service_status` check again (back to "connecting" until it settles). */
|
|
39
|
+
retryStatus: () => void;
|
|
32
40
|
/** The sign-in gate, ready to place. */
|
|
33
41
|
gate: ReactNode;
|
|
34
42
|
}
|
package/dist/react/AppShell.js
CHANGED
|
@@ -13,20 +13,23 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
13
13
|
* `signedOut` the gate is the page.
|
|
14
14
|
*
|
|
15
15
|
* Inside, `useAppShell()` gives any component the same `shell` — the npub,
|
|
16
|
-
* `signOut`, the `service_status` answer
|
|
17
|
-
*
|
|
16
|
+
* `signOut`, the `service_status` answer and whether that check is still
|
|
17
|
+
* connecting, answered or failed (with `retryStatus()` to ask again) — so a
|
|
18
|
+
* site keeps no session context of its own.
|
|
18
19
|
*
|
|
19
20
|
* Also done here, once: the stored theme applied before paint and followed
|
|
20
21
|
* across tabs; the npub's kind-0 picture seeded as its avatar; `DebugPanel`
|
|
21
22
|
* mounted last, so its spacer is the page's last thing (see `shellLayout`).
|
|
22
23
|
*/
|
|
23
|
-
import { createContext, useContext, useEffect, useLayoutEffect,
|
|
24
|
+
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useReducer, useRef } from "react";
|
|
24
25
|
import { hydrateAvatarFromNostr } from "../avatar.js";
|
|
26
|
+
import { debugPush } from "../debugLog.js";
|
|
25
27
|
import { serviceStatus } from "../standardTools.js";
|
|
26
28
|
import { bootstrapTheme } from "../theme.js";
|
|
27
29
|
import DebugPanel, {} from "./DebugPanel.js";
|
|
28
30
|
import NpubGate from "./NpubGate.js";
|
|
29
31
|
import { shellLayout } from "./shellLayout.js";
|
|
32
|
+
import { STATUS_CONNECTING, statusReducer } from "./statusCheck.js";
|
|
30
33
|
import { useSession } from "./useSession.js";
|
|
31
34
|
import { useTheme } from "./useTheme.js";
|
|
32
35
|
const Ctx = createContext(null);
|
|
@@ -39,27 +42,41 @@ export function useAppShell() {
|
|
|
39
42
|
}
|
|
40
43
|
export default function AppShell({ children, signedOut, footer, debug = {}, theme = "dark", fit = "page", startFresh, classNames: c = {}, }) {
|
|
41
44
|
const session = useSession();
|
|
42
|
-
const [
|
|
45
|
+
const [check, dispatch] = useReducer(statusReducer, STATUS_CONNECTING);
|
|
46
|
+
const attempt = useRef(0);
|
|
43
47
|
useTheme(theme);
|
|
44
48
|
useLayoutEffect(() => {
|
|
45
49
|
bootstrapTheme(theme);
|
|
46
50
|
}, [theme]);
|
|
51
|
+
// Only the newest check settles the state: a slow first answer must not
|
|
52
|
+
// overwrite a retry's, and an unmounted shell settles nothing.
|
|
53
|
+
const retryStatus = useCallback(() => {
|
|
54
|
+
const mine = ++attempt.current;
|
|
55
|
+
dispatch({ type: "start" });
|
|
56
|
+
serviceStatus({ bestEffort: true }).then((s) => {
|
|
57
|
+
if (attempt.current === mine)
|
|
58
|
+
dispatch({ type: "ok", status: s });
|
|
59
|
+
}, (e) => {
|
|
60
|
+
// service_status is a quiet call, so its failure is logged here.
|
|
61
|
+
debugPush("error", `service_status failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
62
|
+
if (attempt.current === mine)
|
|
63
|
+
dispatch({ type: "fail", error: e });
|
|
64
|
+
});
|
|
65
|
+
}, []);
|
|
47
66
|
useEffect(() => {
|
|
48
|
-
|
|
49
|
-
serviceStatus({ bestEffort: true })
|
|
50
|
-
.then((s) => live && setStatus(s))
|
|
51
|
-
.catch(() => live && setStatus(null));
|
|
67
|
+
retryStatus();
|
|
52
68
|
return () => {
|
|
53
|
-
|
|
69
|
+
attempt.current++;
|
|
54
70
|
};
|
|
55
|
-
}, []);
|
|
71
|
+
}, [retryStatus]);
|
|
56
72
|
const { npub, signedIn, notice, refresh } = session;
|
|
57
73
|
useEffect(() => {
|
|
58
74
|
if (signedIn && npub)
|
|
59
75
|
void hydrateAvatarFromNostr(npub);
|
|
60
76
|
}, [signedIn, npub]);
|
|
77
|
+
const { status, state: statusState, error: statusError } = check;
|
|
61
78
|
const gate = (_jsx(NpubGate, { onLogin: refresh, operatorHash: status?.operator_npub_hash, notice: notice, startFresh: startFresh }));
|
|
62
|
-
const shell = { session, status, gate };
|
|
79
|
+
const shell = { session, status, statusState, statusError, retryStatus, gate };
|
|
63
80
|
const layout = shellLayout(fit);
|
|
64
81
|
return (_jsx(Ctx.Provider, { value: shell, children: _jsxs("div", { className: c.root, style: layout.root, children: [_jsx("div", { className: c.body, style: layout.body, children: signedIn ? children(shell) : signedOut ? signedOut(shell) : gate }), typeof footer === "function" ? footer(shell) : footer, debug !== false && _jsx(DebugPanel, { ...debug })] }) }));
|
|
65
82
|
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as AccountPage, type AccountPageClassNames, type AccountPageProps, type AccountSectionText, type AccountThemeProps, type AccountTimezoneProps, } from "./AccountPage.tsx";
|
|
2
2
|
export { ACCOUNT_SECTIONS, type AccountSection } from "./accountSections.ts";
|
|
3
3
|
export { default as AppShell, useAppShell, type AppShellClassNames, type AppShellContext, type AppShellProps, } from "./AppShell.tsx";
|
|
4
|
+
export { statusErrorMessage, type StatusState } from "./statusCheck.ts";
|
|
4
5
|
export type { ShellFit } from "./shellLayout.ts";
|
|
5
6
|
export { default as Avatar } from "./Avatar.tsx";
|
|
6
7
|
export { default as AvatarPicker } from "./AvatarPicker.tsx";
|
package/dist/react/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as AccountPage, } from "./AccountPage.js";
|
|
2
2
|
export { ACCOUNT_SECTIONS } from "./accountSections.js";
|
|
3
3
|
export { default as AppShell, useAppShell, } from "./AppShell.js";
|
|
4
|
+
export { statusErrorMessage } from "./statusCheck.js";
|
|
4
5
|
export { default as Avatar } from "./Avatar.js";
|
|
5
6
|
export { default as AvatarPicker } from "./AvatarPicker.js";
|
|
6
7
|
export { default as BuildInfoPanel, } from "./BuildInfoPanel.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell's `service_status` check as three states a site can tell apart:
|
|
3
|
+
* still connecting, answered, or failed. Before this a failed check read as
|
|
4
|
+
* "no answer yet", so a banner said "Connecting…" through a whole outage.
|
|
5
|
+
*
|
|
6
|
+
* Pure: `AppShell` feeds it events, and the tests feed it the same ones.
|
|
7
|
+
*/
|
|
8
|
+
import type { ServiceStatus } from "../standardTools.ts";
|
|
9
|
+
export type StatusState = "connecting" | "ready" | "failed";
|
|
10
|
+
export interface StatusCheck {
|
|
11
|
+
state: StatusState;
|
|
12
|
+
/** The last `service_status` answer; kept through a retry. */
|
|
13
|
+
status: ServiceStatus | null;
|
|
14
|
+
/** Why the last check failed — short, human, scrubbed — or null. */
|
|
15
|
+
error: string | null;
|
|
16
|
+
}
|
|
17
|
+
export type StatusEvent = {
|
|
18
|
+
type: "start";
|
|
19
|
+
} | {
|
|
20
|
+
type: "ok";
|
|
21
|
+
status: ServiceStatus;
|
|
22
|
+
} | {
|
|
23
|
+
type: "fail";
|
|
24
|
+
error: unknown;
|
|
25
|
+
};
|
|
26
|
+
export declare const STATUS_CONNECTING: StatusCheck;
|
|
27
|
+
/** The longest `statusError` a banner is handed. */
|
|
28
|
+
export declare const STATUS_ERROR_MAX = 140;
|
|
29
|
+
/**
|
|
30
|
+
* A failed check as one short line: scrubbed of secrets, the runtime tool
|
|
31
|
+
* name dropped, whitespace collapsed, cut at `STATUS_ERROR_MAX`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function statusErrorMessage(error: unknown): string;
|
|
34
|
+
export declare function statusReducer(check: StatusCheck, event: StatusEvent): StatusCheck;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell's `service_status` check as three states a site can tell apart:
|
|
3
|
+
* still connecting, answered, or failed. Before this a failed check read as
|
|
4
|
+
* "no answer yet", so a banner said "Connecting…" through a whole outage.
|
|
5
|
+
*
|
|
6
|
+
* Pure: `AppShell` feeds it events, and the tests feed it the same ones.
|
|
7
|
+
*/
|
|
8
|
+
import { redact } from "../debugLog.js";
|
|
9
|
+
import { isNetworkError } from "../networkError.js";
|
|
10
|
+
export const STATUS_CONNECTING = { state: "connecting", status: null, error: null };
|
|
11
|
+
/** The longest `statusError` a banner is handed. */
|
|
12
|
+
export const STATUS_ERROR_MAX = 140;
|
|
13
|
+
/**
|
|
14
|
+
* A failed check as one short line: scrubbed of secrets, the runtime tool
|
|
15
|
+
* name dropped, whitespace collapsed, cut at `STATUS_ERROR_MAX`.
|
|
16
|
+
*/
|
|
17
|
+
export function statusErrorMessage(error) {
|
|
18
|
+
const raw = error instanceof Error ? error.message : String(error ?? "");
|
|
19
|
+
const why = redact(raw)
|
|
20
|
+
.replace(/^[a-z0-9_]*service_status:\s*/i, "")
|
|
21
|
+
.replace(/\s+/g, " ")
|
|
22
|
+
.trim();
|
|
23
|
+
const line = isNetworkError(error)
|
|
24
|
+
? why
|
|
25
|
+
? `Could not reach the service — ${why}`
|
|
26
|
+
: "Could not reach the service"
|
|
27
|
+
: why || "The service did not answer";
|
|
28
|
+
return line.length > STATUS_ERROR_MAX ? `${line.slice(0, STATUS_ERROR_MAX - 1)}…` : line;
|
|
29
|
+
}
|
|
30
|
+
export function statusReducer(check, event) {
|
|
31
|
+
switch (event.type) {
|
|
32
|
+
case "start":
|
|
33
|
+
return { state: "connecting", status: check.status, error: null };
|
|
34
|
+
case "ok":
|
|
35
|
+
return { state: "ready", status: event.status, error: null };
|
|
36
|
+
case "fail":
|
|
37
|
+
return { state: "failed", status: check.status, error: statusErrorMessage(event.error) };
|
|
38
|
+
}
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tollbooth-dpyc/web",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Shared browser SDK for Tollbooth DPYC operator front ends: npub sign-in, proofs, MCP client, profile and wallet",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Lonnie VanZandt",
|