cursedbelt 5.1.0 → 5.2.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/dist/react/engagement/beacon.d.ts +38 -0
- package/dist/react/engagement/beacon.d.ts.map +1 -0
- package/dist/react/engagement/beacon.js +62 -0
- package/dist/react/engagement/beacon.js.map +1 -0
- package/dist/react/engagement/beaconClient.d.ts +119 -0
- package/dist/react/engagement/beaconClient.d.ts.map +1 -0
- package/dist/react/engagement/beaconClient.js +203 -0
- package/dist/react/engagement/beaconClient.js.map +1 -0
- package/dist/react/engagement/index.d.ts +3 -0
- package/dist/react/engagement/index.d.ts.map +1 -0
- package/dist/react/engagement/index.js +5 -0
- package/dist/react/engagement/index.js.map +1 -0
- package/dist/react/lib/directUpload.d.ts +130 -0
- package/dist/react/lib/directUpload.d.ts.map +1 -0
- package/dist/react/lib/directUpload.js +228 -0
- package/dist/react/lib/directUpload.js.map +1 -0
- package/dist/styles-areas/auth.css +2 -2
- package/dist/styles-areas/core.css +2 -2
- package/package.json +13 -1
- package/scripts/checkAreaStyles.spec.ts +23 -0
- package/scripts/checkAreaStyles.ts +20 -1
- package/scripts/styleAreas.ts +2 -0
- package/src/react/engagement/beacon.tsx +63 -0
- package/src/react/engagement/beaconClient.spec.ts +138 -0
- package/src/react/engagement/beaconClient.ts +228 -0
- package/src/react/engagement/index.ts +13 -0
- package/src/react/lib/directUpload.spec.ts +128 -0
- package/src/react/lib/directUpload.ts +334 -0
- package/src/styles-areas/auth.css +2 -2
- package/src/styles-areas/core.css +2 -2
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type ReportPlaceOptions } from "./beaconClient.js";
|
|
2
|
+
/**
|
|
3
|
+
* Report `place` whenever it changes.
|
|
4
|
+
*
|
|
5
|
+
* `place` should be the app's own route-manifest id — the same string the
|
|
6
|
+
* server resolves against. Pass `null` while the route is unresolved (a lazy
|
|
7
|
+
* view still loading, a signed-out shell) rather than a placeholder: a
|
|
8
|
+
* placeholder becomes a real row in the console.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useEngagementBeacon(place: string | null, options?: ReportPlaceOptions): void;
|
|
11
|
+
/**
|
|
12
|
+
* Report wherever the browser IS, and keep reporting as it moves.
|
|
13
|
+
*
|
|
14
|
+
* This is the form every app should use, and it is why adoption is one line
|
|
15
|
+
* rather than a per-app mapping: the client sends its LOCATION and the server
|
|
16
|
+
* resolves it against that app's own route manifest (`policy.ts:resolvePlace`).
|
|
17
|
+
* A `route.view → place id` table written in twelve app shells would be twelve
|
|
18
|
+
* places for the names to drift, and a drifted name reads on the board as a
|
|
19
|
+
* page nobody opens — a wrong answer, not a gap.
|
|
20
|
+
*
|
|
21
|
+
* Listens to `hashchange` and `popstate`, which between them cover both kinds of
|
|
22
|
+
* router — hash-routed apps like this one, and a pushState app. `reportPlace`
|
|
23
|
+
* de-duplicates, so a re-render costs nothing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useLocationBeacon(options?: ReportPlaceOptions): void;
|
|
26
|
+
/**
|
|
27
|
+
* The location beacon as an element, for a shell that mounts declaratively.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately takes no `place` prop. Offering both would mean two callers of
|
|
30
|
+
* one module-level de-duplication memory, and an app that passed a place would
|
|
31
|
+
* silently suppress the location half or vice versa — use
|
|
32
|
+
* {@link useEngagementBeacon} directly when an app really does know its own
|
|
33
|
+
* place ids.
|
|
34
|
+
*/
|
|
35
|
+
export declare function EngagementBeacon(props: {
|
|
36
|
+
basePath?: string;
|
|
37
|
+
}): null;
|
|
38
|
+
//# sourceMappingURL=beacon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"beacon.d.ts","sourceRoot":"","sources":["../../../src/react/engagement/beacon.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,kBAAkB,EAAoC,MAAM,mBAAmB,CAAC;AAE9F;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,GAAE,kBAAuB,GAAG,IAAI,CAQhG;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,IAAI,CAMxE;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAGnE"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The React adapter over `beaconClient.ts` — one hook, one component.
|
|
3
|
+
*
|
|
4
|
+
* Separate file from the plain-JS half so an app with no React (or a test with
|
|
5
|
+
* no DOM) can import `reportPlace` without pulling React in, and so the app's
|
|
6
|
+
* own router stays the thing that decides what "a place" is.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect } from "react";
|
|
9
|
+
import { reportPlace, startLocationBeacon } from "./beaconClient.js";
|
|
10
|
+
/**
|
|
11
|
+
* Report `place` whenever it changes.
|
|
12
|
+
*
|
|
13
|
+
* `place` should be the app's own route-manifest id — the same string the
|
|
14
|
+
* server resolves against. Pass `null` while the route is unresolved (a lazy
|
|
15
|
+
* view still loading, a signed-out shell) rather than a placeholder: a
|
|
16
|
+
* placeholder becomes a real row in the console.
|
|
17
|
+
*/
|
|
18
|
+
export function useEngagementBeacon(place, options = {}) {
|
|
19
|
+
const basePath = options.basePath;
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!place)
|
|
22
|
+
return;
|
|
23
|
+
// Fire and forget; `reportPlace` never rejects. Not awaited on purpose —
|
|
24
|
+
// a route change must not wait on telemetry.
|
|
25
|
+
void reportPlace(place, basePath ? { basePath } : {});
|
|
26
|
+
}, [place, basePath]);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Report wherever the browser IS, and keep reporting as it moves.
|
|
30
|
+
*
|
|
31
|
+
* This is the form every app should use, and it is why adoption is one line
|
|
32
|
+
* rather than a per-app mapping: the client sends its LOCATION and the server
|
|
33
|
+
* resolves it against that app's own route manifest (`policy.ts:resolvePlace`).
|
|
34
|
+
* A `route.view → place id` table written in twelve app shells would be twelve
|
|
35
|
+
* places for the names to drift, and a drifted name reads on the board as a
|
|
36
|
+
* page nobody opens — a wrong answer, not a gap.
|
|
37
|
+
*
|
|
38
|
+
* Listens to `hashchange` and `popstate`, which between them cover both kinds of
|
|
39
|
+
* router — hash-routed apps like this one, and a pushState app. `reportPlace`
|
|
40
|
+
* de-duplicates, so a re-render costs nothing.
|
|
41
|
+
*/
|
|
42
|
+
export function useLocationBeacon(options = {}) {
|
|
43
|
+
const basePath = options.basePath;
|
|
44
|
+
// Delegates to the plain-JS starter so there is ONE implementation of the
|
|
45
|
+
// listener set. Most apps call `startLocationBeacon()` from `main.tsx`
|
|
46
|
+
// instead; this exists for a shell that would rather own the lifetime.
|
|
47
|
+
useEffect(() => startLocationBeacon(basePath ? { basePath } : {}), [basePath]);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The location beacon as an element, for a shell that mounts declaratively.
|
|
51
|
+
*
|
|
52
|
+
* Deliberately takes no `place` prop. Offering both would mean two callers of
|
|
53
|
+
* one module-level de-duplication memory, and an app that passed a place would
|
|
54
|
+
* silently suppress the location half or vice versa — use
|
|
55
|
+
* {@link useEngagementBeacon} directly when an app really does know its own
|
|
56
|
+
* place ids.
|
|
57
|
+
*/
|
|
58
|
+
export function EngagementBeacon(props) {
|
|
59
|
+
useLocationBeacon(props.basePath ? { basePath: props.basePath } : {});
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=beacon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"beacon.js","sourceRoot":"","sources":["../../../src/react/engagement/beacon.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAA2B,WAAW,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE9F;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAoB,EAAE,UAA8B,EAAE;IACzF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,yEAAyE;QACzE,6CAA6C;QAC7C,KAAK,WAAW,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACvD,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;AACvB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA8B,EAAE;IACjE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,0EAA0E;IAC1E,uEAAuE;IACvE,uEAAuE;IACvE,SAAS,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAA4B;IAC5D,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACtE,OAAO,IAAI,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt/react/engagement-beacon` — the engagement beacon's plain-JS half (no React). Lifted
|
|
3
|
+
* 2026-09-23 (cursedbelt 5.2.0, task 098-280's browser half) from `src/kit/engagement/beacon.client.ts`,
|
|
4
|
+
* identical in collections, family, music and roms (auth's differed only in a comment). The
|
|
5
|
+
* server half is `cursedbelt-server/engagement`.
|
|
6
|
+
*
|
|
7
|
+
* The browser half — report which PLACE the user is on, once per arrival.
|
|
8
|
+
*
|
|
9
|
+
* ── Why an explicit beacon and not the request log ──────────────────────────
|
|
10
|
+
* Most satellites are SPA-routed: moving from Notes' Files page to its Ask page
|
|
11
|
+
* fires zero navigations and, on a warm cache, zero HTTP requests. So the
|
|
12
|
+
* request metrics already in `metrics/` can tell you an app is used and cannot
|
|
13
|
+
* tell you which of its pages are — which is exactly the question the owner
|
|
14
|
+
* asked ("notes is used but its slideshow is not"). One small event, sent by
|
|
15
|
+
* the router, is the only thing that can answer it.
|
|
16
|
+
*
|
|
17
|
+
* ── It is ONE implementation, adopted; never per-app ────────────────────────
|
|
18
|
+
* Eleven hand-rolled beacons would be eleven different place vocabularies, and
|
|
19
|
+
* the console would be comparing strings that mean different things. Apps call
|
|
20
|
+
* `reportPlace` (or mount `useEngagementBeacon`) and send an id from their own
|
|
21
|
+
* route manifest; the server resolves it against that same manifest.
|
|
22
|
+
*
|
|
23
|
+
* ── What it does NOT send ───────────────────────────────────────────────────
|
|
24
|
+
* No path, no query, no title, no referrer, no identifiers of any kind — just
|
|
25
|
+
* `{ place }`. The account comes from the session cookie the request already
|
|
26
|
+
* carries, server-side. There is nothing in the payload that could carry a note
|
|
27
|
+
* id, a ROM title or a search string, which is the property that makes this
|
|
28
|
+
* safe to run inside private apps at all. See `policy.ts`.
|
|
29
|
+
*
|
|
30
|
+
* ── Failure is silence ──────────────────────────────────────────────────────
|
|
31
|
+
* Every call is fire-and-forget and swallows its error. Telemetry that can
|
|
32
|
+
* surface an error toast, block a route change, or reject an unhandled promise
|
|
33
|
+
* is telemetry that makes the app worse than having none.
|
|
34
|
+
*
|
|
35
|
+
* 🔴 …and "silence" includes the BROWSER's console, which is why the beacon
|
|
36
|
+
* asks whether there is a session before it reports (`SESSION_PATH` below).
|
|
37
|
+
* Swallowing the rejection in JS is not enough: a `fetch` that comes back 401
|
|
38
|
+
* leaves the browser's own *"Failed to load resource: the server responded with
|
|
39
|
+
* a status of 401"* in the console no matter what the caller does with the
|
|
40
|
+
* promise. Measured 2026-08-14 against live prod — `music` and `learn` both
|
|
41
|
+
* failed their signed-out route-health check on exactly this, with a correctly
|
|
42
|
+
* rendered sign-in card on screen and nothing else wrong. `apps/roms`' App.tsx
|
|
43
|
+
* already wrote the rule down for the feedback provider: *"an app that logs an
|
|
44
|
+
* error on its front door is an app nobody can tell apart from a broken one."*
|
|
45
|
+
* The recorder's 401 is deliberate and stays — `api.ts` mounts it AFTER the
|
|
46
|
+
* session gate on purpose, because the gate is what puts the user where
|
|
47
|
+
* `resolveUser` can read them — so the fix belongs on this side.
|
|
48
|
+
*
|
|
49
|
+
* It is also the semantically right answer, not merely a quieter one: the
|
|
50
|
+
* recorder attributes a view to an account, so a signed-out arrival could never
|
|
51
|
+
* have been recorded. Nothing that used to be counted stops being counted.
|
|
52
|
+
*/
|
|
53
|
+
/** Where the recorder is mounted. Apps that remount it pass `basePath`. */
|
|
54
|
+
export declare const ENGAGEMENT_BASE_PATH = "/api/engagement";
|
|
55
|
+
/**
|
|
56
|
+
* The public session probe every satellite serves
|
|
57
|
+
* (`ssoConsumer.server.ts` → `{ authenticated, user?, accountsUrl }`).
|
|
58
|
+
*
|
|
59
|
+
* Deliberately NOT session-gated on the server, which is what makes it usable
|
|
60
|
+
* here: asking it costs one small same-origin GET and can never itself produce
|
|
61
|
+
* the console line this exists to avoid.
|
|
62
|
+
*/
|
|
63
|
+
export declare const SESSION_PATH = "/api/auth/session";
|
|
64
|
+
export interface ReportPlaceOptions {
|
|
65
|
+
basePath?: string;
|
|
66
|
+
/** Injected in tests; defaults to the global. */
|
|
67
|
+
fetchImpl?: typeof fetch;
|
|
68
|
+
/** Override the session probe. Apps that remount the consumer elsewhere. */
|
|
69
|
+
sessionPath?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Skip the session probe and report unconditionally.
|
|
72
|
+
*
|
|
73
|
+
* For a caller that ALREADY knows there is a session — a React tree below the
|
|
74
|
+
* authenticated branch, say — where the probe would be a second answer to a
|
|
75
|
+
* question already asked. Never set it from `main.tsx`, which runs before
|
|
76
|
+
* anything knows.
|
|
77
|
+
*/
|
|
78
|
+
assumeSignedIn?: boolean;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Where the browser is, as a string the server can resolve.
|
|
82
|
+
*
|
|
83
|
+
* The hash when there is one (every satellite but roms routes on it), else the
|
|
84
|
+
* pathname. Never the search string: `normalizePlaceId` strips one anyway, and
|
|
85
|
+
* not sending it in the first place means user text does not travel at all.
|
|
86
|
+
*/
|
|
87
|
+
export declare function currentLocationPlace(): string;
|
|
88
|
+
/** Reset the de-duplication memory. Tests, and a sign-out. */
|
|
89
|
+
export declare function resetEngagementBeacon(): void;
|
|
90
|
+
/**
|
|
91
|
+
* Report an arrival at `place`. Returns whether anything was sent.
|
|
92
|
+
*
|
|
93
|
+
* Not awaited by callers in practice — the promise is returned so a test can
|
|
94
|
+
* settle it without a timer.
|
|
95
|
+
*/
|
|
96
|
+
export declare function reportPlace(place: string, options?: ReportPlaceOptions): Promise<boolean>;
|
|
97
|
+
/** Detaches the listeners `startLocationBeacon` installed. */
|
|
98
|
+
export type StopBeacon = () => void;
|
|
99
|
+
/**
|
|
100
|
+
* Start reporting wherever the browser is, and keep reporting as it moves.
|
|
101
|
+
*
|
|
102
|
+
* The form every app adopts, called once from `main.tsx` beside
|
|
103
|
+
* `initColorScheme()` — one line, no React, and no per-app mapping from a
|
|
104
|
+
* router's internal view name to a place id. The server resolves the LOCATION
|
|
105
|
+
* against that app's own `routes.manifest.json` (`policy.ts:resolvePlace`),
|
|
106
|
+
* which is the list the app already maintains and already gates on.
|
|
107
|
+
*
|
|
108
|
+
* `hashchange` and `popstate` between them cover both kinds of router, which is
|
|
109
|
+
* why BOTH are listened for when `collections` — hash-routed — only needs the
|
|
110
|
+
* first. The census that settled it was the previous generation's twelve apps:
|
|
111
|
+
* eleven hash routers and one pushState app. Keeping the second listener costs
|
|
112
|
+
* a no-op call, and losing it would be silent.
|
|
113
|
+
*
|
|
114
|
+
* Idempotent — a second call replaces the first rather than doubling every
|
|
115
|
+
* view, which matters because React 18 StrictMode mounts twice in development
|
|
116
|
+
* and a doubled count is a wrong number rather than a visible fault.
|
|
117
|
+
*/
|
|
118
|
+
export declare function startLocationBeacon(options?: ReportPlaceOptions): StopBeacon;
|
|
119
|
+
//# sourceMappingURL=beaconClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"beaconClient.d.ts","sourceRoot":"","sources":["../../../src/react/engagement/beaconClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AAEtD;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,sBAAsB,CAAC;AAEhD,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAaD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAI7C;AAYD,8DAA8D;AAC9D,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAyBD;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,kBAAuB,GAC9B,OAAO,CAAC,OAAO,CAAC,CA+BlB;AAED,8DAA8D;AAC9D,MAAM,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC;AAIpC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAehF"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt/react/engagement-beacon` — the engagement beacon's plain-JS half (no React). Lifted
|
|
3
|
+
* 2026-09-23 (cursedbelt 5.2.0, task 098-280's browser half) from `src/kit/engagement/beacon.client.ts`,
|
|
4
|
+
* identical in collections, family, music and roms (auth's differed only in a comment). The
|
|
5
|
+
* server half is `cursedbelt-server/engagement`.
|
|
6
|
+
*
|
|
7
|
+
* The browser half — report which PLACE the user is on, once per arrival.
|
|
8
|
+
*
|
|
9
|
+
* ── Why an explicit beacon and not the request log ──────────────────────────
|
|
10
|
+
* Most satellites are SPA-routed: moving from Notes' Files page to its Ask page
|
|
11
|
+
* fires zero navigations and, on a warm cache, zero HTTP requests. So the
|
|
12
|
+
* request metrics already in `metrics/` can tell you an app is used and cannot
|
|
13
|
+
* tell you which of its pages are — which is exactly the question the owner
|
|
14
|
+
* asked ("notes is used but its slideshow is not"). One small event, sent by
|
|
15
|
+
* the router, is the only thing that can answer it.
|
|
16
|
+
*
|
|
17
|
+
* ── It is ONE implementation, adopted; never per-app ────────────────────────
|
|
18
|
+
* Eleven hand-rolled beacons would be eleven different place vocabularies, and
|
|
19
|
+
* the console would be comparing strings that mean different things. Apps call
|
|
20
|
+
* `reportPlace` (or mount `useEngagementBeacon`) and send an id from their own
|
|
21
|
+
* route manifest; the server resolves it against that same manifest.
|
|
22
|
+
*
|
|
23
|
+
* ── What it does NOT send ───────────────────────────────────────────────────
|
|
24
|
+
* No path, no query, no title, no referrer, no identifiers of any kind — just
|
|
25
|
+
* `{ place }`. The account comes from the session cookie the request already
|
|
26
|
+
* carries, server-side. There is nothing in the payload that could carry a note
|
|
27
|
+
* id, a ROM title or a search string, which is the property that makes this
|
|
28
|
+
* safe to run inside private apps at all. See `policy.ts`.
|
|
29
|
+
*
|
|
30
|
+
* ── Failure is silence ──────────────────────────────────────────────────────
|
|
31
|
+
* Every call is fire-and-forget and swallows its error. Telemetry that can
|
|
32
|
+
* surface an error toast, block a route change, or reject an unhandled promise
|
|
33
|
+
* is telemetry that makes the app worse than having none.
|
|
34
|
+
*
|
|
35
|
+
* 🔴 …and "silence" includes the BROWSER's console, which is why the beacon
|
|
36
|
+
* asks whether there is a session before it reports (`SESSION_PATH` below).
|
|
37
|
+
* Swallowing the rejection in JS is not enough: a `fetch` that comes back 401
|
|
38
|
+
* leaves the browser's own *"Failed to load resource: the server responded with
|
|
39
|
+
* a status of 401"* in the console no matter what the caller does with the
|
|
40
|
+
* promise. Measured 2026-08-14 against live prod — `music` and `learn` both
|
|
41
|
+
* failed their signed-out route-health check on exactly this, with a correctly
|
|
42
|
+
* rendered sign-in card on screen and nothing else wrong. `apps/roms`' App.tsx
|
|
43
|
+
* already wrote the rule down for the feedback provider: *"an app that logs an
|
|
44
|
+
* error on its front door is an app nobody can tell apart from a broken one."*
|
|
45
|
+
* The recorder's 401 is deliberate and stays — `api.ts` mounts it AFTER the
|
|
46
|
+
* session gate on purpose, because the gate is what puts the user where
|
|
47
|
+
* `resolveUser` can read them — so the fix belongs on this side.
|
|
48
|
+
*
|
|
49
|
+
* It is also the semantically right answer, not merely a quieter one: the
|
|
50
|
+
* recorder attributes a view to an account, so a signed-out arrival could never
|
|
51
|
+
* have been recorded. Nothing that used to be counted stops being counted.
|
|
52
|
+
*/
|
|
53
|
+
/** Where the recorder is mounted. Apps that remount it pass `basePath`. */
|
|
54
|
+
export const ENGAGEMENT_BASE_PATH = "/api/engagement";
|
|
55
|
+
/**
|
|
56
|
+
* The public session probe every satellite serves
|
|
57
|
+
* (`ssoConsumer.server.ts` → `{ authenticated, user?, accountsUrl }`).
|
|
58
|
+
*
|
|
59
|
+
* Deliberately NOT session-gated on the server, which is what makes it usable
|
|
60
|
+
* here: asking it costs one small same-origin GET and can never itself produce
|
|
61
|
+
* the console line this exists to avoid.
|
|
62
|
+
*/
|
|
63
|
+
export const SESSION_PATH = "/api/auth/session";
|
|
64
|
+
/**
|
|
65
|
+
* The last place reported, so a router that re-renders does not re-report.
|
|
66
|
+
*
|
|
67
|
+
* A React router re-runs its effects on every state change that touches the
|
|
68
|
+
* route object, and a beacon per keystroke in a search box would turn one visit
|
|
69
|
+
* into forty views — inflating exactly the number the console reads as
|
|
70
|
+
* "interest". De-duplicating on the place id is what keeps a view meaning "an
|
|
71
|
+
* arrival".
|
|
72
|
+
*/
|
|
73
|
+
let lastReported = null;
|
|
74
|
+
/**
|
|
75
|
+
* Where the browser is, as a string the server can resolve.
|
|
76
|
+
*
|
|
77
|
+
* The hash when there is one (every satellite but roms routes on it), else the
|
|
78
|
+
* pathname. Never the search string: `normalizePlaceId` strips one anyway, and
|
|
79
|
+
* not sending it in the first place means user text does not travel at all.
|
|
80
|
+
*/
|
|
81
|
+
export function currentLocationPlace() {
|
|
82
|
+
const { hash, pathname } = window.location;
|
|
83
|
+
const raw = hash && hash !== "#" ? hash : pathname;
|
|
84
|
+
return raw.split("?")[0] ?? "/";
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The answer to "is anyone signed in?", remembered for this page load.
|
|
88
|
+
*
|
|
89
|
+
* One probe per load, not one per navigation: the answer cannot change without
|
|
90
|
+
* a navigation that replaces the document (the SSO callback is a full page
|
|
91
|
+
* load), and re-asking on every hash change would trade one wasted request for
|
|
92
|
+
* dozens.
|
|
93
|
+
*/
|
|
94
|
+
let sessionAnswer = null;
|
|
95
|
+
/** Reset the de-duplication memory. Tests, and a sign-out. */
|
|
96
|
+
export function resetEngagementBeacon() {
|
|
97
|
+
lastReported = null;
|
|
98
|
+
sessionAnswer = null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Is there a session? Silent, cached, and false on any doubt.
|
|
102
|
+
*
|
|
103
|
+
* Fails CLOSED — a probe that throws, answers non-2xx or returns a body this
|
|
104
|
+
* cannot read means "do not report". The cost of a false negative is one
|
|
105
|
+
* uncounted view; the cost of a false positive is the console line on the front
|
|
106
|
+
* door that this whole path exists to prevent.
|
|
107
|
+
*/
|
|
108
|
+
async function hasSession(options) {
|
|
109
|
+
const doFetch = options.fetchImpl ?? globalThis.fetch;
|
|
110
|
+
if (typeof doFetch !== "function")
|
|
111
|
+
return false;
|
|
112
|
+
try {
|
|
113
|
+
const res = await doFetch(options.sessionPath ?? SESSION_PATH, {
|
|
114
|
+
credentials: "same-origin",
|
|
115
|
+
});
|
|
116
|
+
if (!res.ok)
|
|
117
|
+
return false;
|
|
118
|
+
const body = (await res.json());
|
|
119
|
+
return body?.authenticated === true;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Report an arrival at `place`. Returns whether anything was sent.
|
|
127
|
+
*
|
|
128
|
+
* Not awaited by callers in practice — the promise is returned so a test can
|
|
129
|
+
* settle it without a timer.
|
|
130
|
+
*/
|
|
131
|
+
export async function reportPlace(place, options = {}) {
|
|
132
|
+
if (!place || place === lastReported)
|
|
133
|
+
return false;
|
|
134
|
+
const doFetch = options.fetchImpl ?? globalThis.fetch;
|
|
135
|
+
if (typeof doFetch !== "function")
|
|
136
|
+
return false;
|
|
137
|
+
// 🔴 BEFORE the de-duplication is committed, not after. A signed-out arrival
|
|
138
|
+
// must leave no trace at all — recording it as `lastReported` would mean the
|
|
139
|
+
// view that happens once the visitor signs in and lands back on this same
|
|
140
|
+
// place is silently dropped as a duplicate.
|
|
141
|
+
if (options.assumeSignedIn !== true) {
|
|
142
|
+
sessionAnswer ??= hasSession(options);
|
|
143
|
+
if (!(await sessionAnswer))
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (place === lastReported)
|
|
147
|
+
return false;
|
|
148
|
+
lastReported = place;
|
|
149
|
+
try {
|
|
150
|
+
await doFetch(`${options.basePath ?? ENGAGEMENT_BASE_PATH}/view`, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { "content-type": "application/json" },
|
|
153
|
+
// Same-origin credentials: the session cookie is the only identity
|
|
154
|
+
// this request carries, and it is the only one it may carry.
|
|
155
|
+
credentials: "same-origin",
|
|
156
|
+
body: JSON.stringify({ place }),
|
|
157
|
+
keepalive: true,
|
|
158
|
+
});
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// See the header: telemetry never becomes the user's problem.
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let running = null;
|
|
167
|
+
/**
|
|
168
|
+
* Start reporting wherever the browser is, and keep reporting as it moves.
|
|
169
|
+
*
|
|
170
|
+
* The form every app adopts, called once from `main.tsx` beside
|
|
171
|
+
* `initColorScheme()` — one line, no React, and no per-app mapping from a
|
|
172
|
+
* router's internal view name to a place id. The server resolves the LOCATION
|
|
173
|
+
* against that app's own `routes.manifest.json` (`policy.ts:resolvePlace`),
|
|
174
|
+
* which is the list the app already maintains and already gates on.
|
|
175
|
+
*
|
|
176
|
+
* `hashchange` and `popstate` between them cover both kinds of router, which is
|
|
177
|
+
* why BOTH are listened for when `collections` — hash-routed — only needs the
|
|
178
|
+
* first. The census that settled it was the previous generation's twelve apps:
|
|
179
|
+
* eleven hash routers and one pushState app. Keeping the second listener costs
|
|
180
|
+
* a no-op call, and losing it would be silent.
|
|
181
|
+
*
|
|
182
|
+
* Idempotent — a second call replaces the first rather than doubling every
|
|
183
|
+
* view, which matters because React 18 StrictMode mounts twice in development
|
|
184
|
+
* and a doubled count is a wrong number rather than a visible fault.
|
|
185
|
+
*/
|
|
186
|
+
export function startLocationBeacon(options = {}) {
|
|
187
|
+
running?.();
|
|
188
|
+
const send = () => {
|
|
189
|
+
void reportPlace(currentLocationPlace(), options);
|
|
190
|
+
};
|
|
191
|
+
send();
|
|
192
|
+
window.addEventListener("hashchange", send);
|
|
193
|
+
window.addEventListener("popstate", send);
|
|
194
|
+
const stop = () => {
|
|
195
|
+
window.removeEventListener("hashchange", send);
|
|
196
|
+
window.removeEventListener("popstate", send);
|
|
197
|
+
if (running === stop)
|
|
198
|
+
running = null;
|
|
199
|
+
};
|
|
200
|
+
running = stop;
|
|
201
|
+
return stop;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=beaconClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"beaconClient.js","sourceRoot":"","sources":["../../../src/react/engagement/beaconClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { ENGAGEMENT_BASE_PATH, type ReportPlaceOptions, reportPlace, resetEngagementBeacon, SESSION_PATH, type StopBeacon, startLocationBeacon, currentLocationPlace, } from './beaconClient.js';
|
|
2
|
+
export { EngagementBeacon, useEngagementBeacon, useLocationBeacon } from './beacon.js';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/engagement/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,oBAAoB,EACpB,KAAK,kBAAkB,EACvB,WAAW,EACX,qBAAqB,EACrB,YAAY,EACZ,KAAK,UAAU,EACf,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// `cursedbelt/react/engagement-beacon` — the engagement beacon: `reportPlace` and
|
|
2
|
+
// `startLocationBeacon` (plain JS, no React) and the React hooks over them.
|
|
3
|
+
export { ENGAGEMENT_BASE_PATH, reportPlace, resetEngagementBeacon, SESSION_PATH, startLocationBeacon, currentLocationPlace, } from './beaconClient.js';
|
|
4
|
+
export { EngagementBeacon, useEngagementBeacon, useLocationBeacon } from './beacon.js';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/engagement/index.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,4EAA4E;AAC5E,OAAO,EACN,oBAAoB,EAEpB,WAAW,EACX,qBAAqB,EACrB,YAAY,EAEZ,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt/react/upload-direct` — browser-side resumable upload, straight from the file input
|
|
3
|
+
* to binary-server.
|
|
4
|
+
*
|
|
5
|
+
* Lifted 2026-09-23 (cursedbelt 5.2.0, task 056-343's browser half) from `src/kit/directUpload.ts`,
|
|
6
|
+
* identical in collections and family. The server half — minting the session — is
|
|
7
|
+
* `cursedbelt-server/binary-store/upload-session`'s `createUploadSession`.
|
|
8
|
+
*
|
|
9
|
+
* 🔴 THIS FILE RUNS IN A BROWSER. It must never import `node:*`, `binaryStore.ts`, or
|
|
10
|
+
* anything that reaches a private key — see {@link UploadSession}. Keep it dependency-free.
|
|
11
|
+
*
|
|
12
|
+
* ## Why the bytes skip the app server
|
|
13
|
+
*
|
|
14
|
+
* Every satellite's upload route reads the whole body (`formData()` → `arrayBuffer()`) before
|
|
15
|
+
* handing it to binary-server, inside a unit capped at `MemoryMax=300M` on a shared t3.micro
|
|
16
|
+
* that also runs nginx and every other app. That is why the fleet's upload ceiling is 32 MB
|
|
17
|
+
* and why raising the number was never the fix: a 10 GB video does not fit in the process, in
|
|
18
|
+
* the nginx body limit, or in the Cloudflare Tunnel's ~100 MB request wall.
|
|
19
|
+
*
|
|
20
|
+
* So a large file does not go through the app server at all. The app server mints a session
|
|
21
|
+
* token naming exactly one destination key; the browser PUTs bounded parts directly to
|
|
22
|
+
* binary-server, which assembles them; the app server is told afterwards and records the
|
|
23
|
+
* metadata. No byte of video touches the EC2 box, which is the owner's standing rule for
|
|
24
|
+
* video (2026-08-12) and also the only shape that survives the memory limit.
|
|
25
|
+
*
|
|
26
|
+
* ## What makes it survive 10 GB
|
|
27
|
+
*
|
|
28
|
+
* - **Resume.** `GET /upload-chunk` reports which parts already landed, so an upload that
|
|
29
|
+
* dies at 90% resumes instead of restarting. That answer comes from the parts on disk, so
|
|
30
|
+
* it survives a binary-server restart too.
|
|
31
|
+
* - **Bounded concurrency.** A few parts in flight keeps a home upstream saturated without
|
|
32
|
+
* putting the whole file in memory. Only one part's bytes are ever read at a time per slot.
|
|
33
|
+
* - **Per-part retry.** A dropped part costs one part, not the upload.
|
|
34
|
+
* - **Slicing, not reading.** `Blob.slice` is lazy — the 10 GB file is never materialized.
|
|
35
|
+
*/
|
|
36
|
+
/** Everything the browser needs to run an upload, minted server-side by `createUploadSession`
|
|
37
|
+
* in `cursedbelt-server/binary-store/upload-session`. Contains a capability token scoped to ONE key, and no key
|
|
38
|
+
* material — it is safe to hand to the page that will do the uploading. */
|
|
39
|
+
export interface UploadSession {
|
|
40
|
+
/** binary-server's public base, e.g. `https://binary-server.cursedalchemy.com`. */
|
|
41
|
+
baseUrl: string;
|
|
42
|
+
/** App-prefixed destination key, e.g. `family/media/<uuid>`. */
|
|
43
|
+
path: string;
|
|
44
|
+
/** Signed token authorizing this session (carries `sid`, `tc` and the source length `sz`). */
|
|
45
|
+
token: string;
|
|
46
|
+
/** Chunk session id. */
|
|
47
|
+
sid: string;
|
|
48
|
+
/** Total parts the file was split into. */
|
|
49
|
+
totalChunks: number;
|
|
50
|
+
/** Bytes per part — must match what the token's `tc` was computed from. */
|
|
51
|
+
chunkBytes: number;
|
|
52
|
+
}
|
|
53
|
+
export interface DirectUploadOptions {
|
|
54
|
+
/** 0..1, called as parts complete. */
|
|
55
|
+
onProgress?: (fraction: number, bytesSent: number, totalBytes: number) => void;
|
|
56
|
+
/** Parts in flight at once. Default 3 — enough to saturate a home upstream, few enough
|
|
57
|
+
* that a failure retries cheaply. */
|
|
58
|
+
concurrency?: number;
|
|
59
|
+
/** Attempts per part before giving up. Default 5, with backoff. */
|
|
60
|
+
retries?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Called once, before any part is sent, when the store reports less free space than this
|
|
63
|
+
* upload is likely to need.
|
|
64
|
+
*
|
|
65
|
+
* 🔴 A WARNING and nothing else — it cannot stop the upload, and there is deliberately no
|
|
66
|
+
* way for it to. The owner's instruction (2026-08-13) is that space must never block them:
|
|
67
|
+
* *"Remove all blockers for space and only allow warning me. I'll manage space myself
|
|
68
|
+
* beyond that."* Returning `false` from here, or throwing, would quietly reinstate exactly
|
|
69
|
+
* the blocker that was removed.
|
|
70
|
+
*/
|
|
71
|
+
onLowDisk?: (info: {
|
|
72
|
+
freeBytes: number;
|
|
73
|
+
needBytes: number;
|
|
74
|
+
}) => void;
|
|
75
|
+
/** Abort the whole upload (a cancel button, or unmount). */
|
|
76
|
+
signal?: AbortSignal;
|
|
77
|
+
/** Re-mint an expired session token mid-upload. A 10 GB upload can outlive any TTL short
|
|
78
|
+
* enough to be safe, so the page supplies a way to ask its own server for a fresh one
|
|
79
|
+
* for the SAME `sid`. Without it, a long upload simply fails on expiry. */
|
|
80
|
+
refreshToken?: () => Promise<string>;
|
|
81
|
+
}
|
|
82
|
+
/** What binary-server reports once the last part lands. */
|
|
83
|
+
export interface DirectUploadResult {
|
|
84
|
+
key: string;
|
|
85
|
+
size: number;
|
|
86
|
+
checksum: string | null;
|
|
87
|
+
}
|
|
88
|
+
/** Split `[0, total)` into `chunkBytes`-sized part indices. */
|
|
89
|
+
export declare const chunkCount: (totalBytes: number, chunkBytes: number) => number;
|
|
90
|
+
/** Which parts binary-server already holds, plus whether the object is already complete.
|
|
91
|
+
* A fresh session answers `{ received: [], complete: false }`, so callers need no special
|
|
92
|
+
* case for "first attempt". */
|
|
93
|
+
export declare function fetchSessionState(session: UploadSession, token?: string, signal?: AbortSignal): Promise<{
|
|
94
|
+
received: Set<number>;
|
|
95
|
+
complete: boolean;
|
|
96
|
+
size: number | null;
|
|
97
|
+
checksum: string | null;
|
|
98
|
+
/** Bytes binary-server still has room for, or null if it could not tell. */
|
|
99
|
+
freeBytes: number | null;
|
|
100
|
+
}>;
|
|
101
|
+
/** Thrown when binary-server has no room for the file. Distinct from a transport failure so a
|
|
102
|
+
* UI can say "the server is full" — which is actionable — rather than "upload failed", which
|
|
103
|
+
* invites the person to retry forever against a disk that will never have space. */
|
|
104
|
+
export declare class InsufficientStorageError extends Error {
|
|
105
|
+
readonly needBytes: number;
|
|
106
|
+
readonly freeBytes: number | null;
|
|
107
|
+
/**
|
|
108
|
+
* 🔴 Vestigial since 2026-08-13 and kept only so an older deployed binary-server can
|
|
109
|
+
* still be understood: THIS service no longer refuses an upload for disk space at all,
|
|
110
|
+
* so nothing here should be producing an overridable refusal any more. If you see one,
|
|
111
|
+
* the origin is running a build from before that change.
|
|
112
|
+
*/
|
|
113
|
+
readonly overridable: boolean;
|
|
114
|
+
constructor(needBytes: number, freeBytes: number | null,
|
|
115
|
+
/**
|
|
116
|
+
* 🔴 Vestigial since 2026-08-13 and kept only so an older deployed binary-server can
|
|
117
|
+
* still be understood: THIS service no longer refuses an upload for disk space at all,
|
|
118
|
+
* so nothing here should be producing an overridable refusal any more. If you see one,
|
|
119
|
+
* the origin is running a build from before that change.
|
|
120
|
+
*/
|
|
121
|
+
overridable?: boolean);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Upload `file` to binary-server in parts, resuming anything already delivered.
|
|
125
|
+
*
|
|
126
|
+
* Resolves once every part has landed and bs has assembled the object. The caller then tells
|
|
127
|
+
* its own app server to record the metadata — see each app's `/complete` route.
|
|
128
|
+
*/
|
|
129
|
+
export declare function uploadDirect(file: Blob, session: UploadSession, opts?: DirectUploadOptions): Promise<DirectUploadResult>;
|
|
130
|
+
//# sourceMappingURL=directUpload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directUpload.d.ts","sourceRoot":"","sources":["../../../src/react/lib/directUpload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH;;4EAE4E;AAC5E,MAAM,WAAW,aAAa;IAC7B,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,8FAA8F;IAC9F,KAAK,EAAE,MAAM,CAAC;IACd,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IACnC,sCAAsC;IACtC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/E;0CACsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,4DAA4D;IAC5D,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;gFAE4E;IAC5E,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACrC;AAED,2DAA2D;AAC3D,MAAM,WAAW,kBAAkB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAID,+DAA+D;AAC/D,eAAO,MAAM,UAAU,GAAI,YAAY,MAAM,EAAE,YAAY,MAAM,KAAG,MACpB,CAAC;AAcjD;;gCAEgC;AAChC,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,aAAa,EACtB,KAAK,SAAgB,EACrB,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC;IACV,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC,CAwBD;AAED;;qFAEqF;AACrF,qBAAa,wBAAyB,SAAQ,KAAK;IAEjD,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW;gBARX,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAAG,IAAI;IACjC;;;;;OAKG;IACM,WAAW,UAAQ;CAS7B;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CACjC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,aAAa,EACtB,IAAI,GAAE,mBAAwB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,CAyJ7B"}
|