@wix/himalaya-cli 0.911.0 → 0.913.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.
Files changed (49) hide show
  1. package/dist/cli.mjs +5 -7
  2. package/guides/getting-started.md +3 -3
  3. package/guides/index.json +1 -1
  4. package/package.json +2 -2
  5. package/templates/beauty-salon/tier5-src/_shared/owner-load-error.ts +68 -7
  6. package/templates/beauty-salon-owner/tier5-src/_shared/owner-load-error.ts +68 -7
  7. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-analytics/index.ts +34 -16
  8. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-analytics/report.ts +14 -3
  9. package/templates/branded-lite/tier5-src/_shared/owner-load-error.ts +68 -7
  10. package/templates/community-club/tier5-src/_shared/owner-load-error.ts +68 -7
  11. package/templates/community-club-owner/tier5-src/_shared/owner-load-error.ts +68 -7
  12. package/templates/community-club-owner/tier5-src/himi-owner-lite-analytics/index.ts +34 -16
  13. package/templates/community-club-owner/tier5-src/himi-owner-lite-analytics/report.ts +14 -3
  14. package/templates/events-meetups/tier5-src/_shared/owner-load-error.ts +68 -7
  15. package/templates/events-meetups-owner/tier5-src/_shared/owner-load-error.ts +68 -7
  16. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-analytics/index.ts +34 -16
  17. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-analytics/report.ts +14 -3
  18. package/templates/gym-fitness/tier5-src/_shared/owner-load-error.ts +68 -7
  19. package/templates/gym-fitness-owner/tier5-src/_shared/owner-load-error.ts +68 -7
  20. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-analytics/index.ts +34 -16
  21. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-analytics/report.ts +14 -3
  22. package/templates/himi-chat/tier5-src/_shared/owner-load-error.ts +68 -7
  23. package/templates/nonprofit-cause/tier5-src/_shared/owner-load-error.ts +68 -7
  24. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-load-error.ts +68 -7
  25. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-analytics/index.ts +34 -16
  26. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-analytics/report.ts +14 -3
  27. package/templates/owner-lite/tier5-src/_shared/owner-load-error.ts +68 -7
  28. package/templates/owner-lite/tier5-src/himi-owner-lite-analytics/index.ts +34 -16
  29. package/templates/owner-lite/tier5-src/himi-owner-lite-analytics/report.ts +14 -3
  30. package/templates/podcast-creator/tier5-src/_shared/owner-load-error.ts +68 -7
  31. package/templates/podcast-creator-owner/tier5-src/_shared/owner-load-error.ts +68 -7
  32. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-analytics/index.ts +34 -16
  33. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-analytics/report.ts +14 -3
  34. package/templates/portfolio-creative/tier5-src/_shared/owner-load-error.ts +68 -7
  35. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-load-error.ts +68 -7
  36. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-analytics/index.ts +34 -16
  37. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-analytics/report.ts +14 -3
  38. package/templates/restaurant/tier5-src/_shared/owner-load-error.ts +68 -7
  39. package/templates/restaurant-owner/tier5-src/_shared/owner-load-error.ts +68 -7
  40. package/templates/restaurant-owner/tier5-src/himi-owner-lite-analytics/index.ts +34 -16
  41. package/templates/restaurant-owner/tier5-src/himi-owner-lite-analytics/report.ts +14 -3
  42. package/templates/retail-store/tier5-src/_shared/owner-load-error.ts +68 -7
  43. package/templates/retail-store-owner/tier5-src/_shared/owner-load-error.ts +68 -7
  44. package/templates/retail-store-owner/tier5-src/himi-owner-lite-analytics/index.ts +34 -16
  45. package/templates/retail-store-owner/tier5-src/himi-owner-lite-analytics/report.ts +14 -3
  46. package/templates/yoga-studio/tier5-src/_shared/owner-load-error.ts +68 -7
  47. package/templates/yoga-studio-owner/tier5-src/_shared/owner-load-error.ts +68 -7
  48. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-analytics/index.ts +34 -16
  49. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-analytics/report.ts +14 -3
package/dist/cli.mjs CHANGED
@@ -18526,7 +18526,6 @@ function shouldRevealError(args) {
18526
18526
  var EMAIL_RE, URL_HOST_RE, INTEGER_RE, NUMBER_RE;
18527
18527
  var init_validation = __esm({
18528
18528
  "../../core/ts/src/validation.ts"() {
18529
- "use strict";
18530
18529
  EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
18531
18530
  URL_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(:[0-9]+)?$/;
18532
18531
  INTEGER_RE = /^[+-]?[0-9]+$/;
@@ -21950,7 +21949,6 @@ function validateWorkerSubtree(raw, slotId, policy, actionCatalog) {
21950
21949
  var WORKER_SUBTREE_CEILINGS, ID_PART, ACTION_ID;
21951
21950
  var init_worker_subtree = __esm({
21952
21951
  "../../core/ts/src/worker-subtree.ts"() {
21953
- "use strict";
21954
21952
  init_worker_subtree_safe_types_generated();
21955
21953
  WORKER_SUBTREE_CEILINGS = Object.freeze({
21956
21954
  maxEncodedBytes: 65536,
@@ -53930,7 +53928,7 @@ ${guardrails.map((g) => `- ${g}`).join("\n")}
53930
53928
  descriptor pair; shows the *wiring* (state, \`$service\` bindings, actions).
53931
53929
  - \`examples/stocks-dashboard.descriptor.json\` \u2014 a fully-composed **real screen** (a Portfolio
53932
53930
  dashboard) to study for layout, spacing rhythm, and semantic color. This is what "good" looks like.
53933
- - Full guides: run \`himi docs\` (list) / \`himi docs <topic>\`, or open the developer portal:
53931
+ - Full guides: run \`himi docs\` (list) / \`himi docs <topic>\`, or read them in a browser:
53934
53932
  ${idx.portalUrl}
53935
53933
  `;
53936
53934
  }
@@ -54119,7 +54117,7 @@ var PRODUCED_HINTS = {
54119
54117
  unbuilt: "this package has no dist/bundles, so there is nothing to measure \u2014 run `himi build` first. A package with no Tier-5 bundle can declare no widget at all.",
54120
54118
  "producer-unavailable": "the widget producer is not resolvable in this install, so production cannot be measured here at all \u2014 building this package will not change that. Ask a running app instead: GET /v1/widgets."
54121
54119
  };
54122
- var DEVELOPER_PORTAL_URL = `${ADMIN_ORIGIN_BO}/#/developer`;
54120
+ var DEVELOPER_PORTAL_URL = `${SERVE_ORIGIN_PUBLIC_APPS}/docs`;
54123
54121
  var MAX_APP_SOURCE_BYTES_HELP = "4 MiB (4,194,304 bytes)";
54124
54122
  var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
54125
54123
 
@@ -54596,7 +54594,7 @@ Learn (works offline \u2014 no serve stack needed):
54596
54594
  Content dir defaults to the current directory (its himalaya.content.json / dir name is the app id).
54597
54595
  Transport: same targets/tokens as himi-serve \u2014 --target <local|prod|bo>, --serve-url/--control-url/
54598
54596
  --telemetry-url, --control-token/--telemetry-token, or HIMI_* env.
54599
- Developer portal: ${DEVELOPER_PORTAL_URL}`;
54597
+ Guides: ${DEVELOPER_PORTAL_URL}`;
54600
54598
  var ONLY_USAGE = "himi push --only <selector>[,<selector>\u2026] (--draft|--live) [--from-release <id>] [--accept-regressions] [--accept-partial]";
54601
54599
  var PATCH_HELP = `himi patch \u2014 change ONE item of a published app, from a raw file, on a machine
54602
54600
  that does not have the content package.
@@ -55458,7 +55456,7 @@ ${cat}`);
55458
55456
  io.log("\n\u2014\u2014\u2014\n");
55459
55457
  io.log(readGuide("headless-loop") ?? "");
55460
55458
  io.log(`
55461
- Developer portal: ${idx.portalUrl}`);
55459
+ Guides in your browser: ${idx.portalUrl}`);
55462
55460
  io.log("\nTopics \u2014 read one with `himi docs <id>`:");
55463
55461
  listByCategory();
55464
55462
  return 0;
@@ -55568,7 +55566,7 @@ Developer portal: ${idx.portalUrl}`);
55568
55566
  io.log("Himalaya authoring topics you can learn \u2014 read one with `himi docs <id>`:");
55569
55567
  listByCategory();
55570
55568
  io.log(`
55571
- Seed your coding agent with \`himi agent-init\`. Portal: ${idx.portalUrl}`);
55569
+ Seed your coding agent with \`himi agent-init\`. Guides: ${idx.portalUrl}`);
55572
55570
  return 0;
55573
55571
  }
55574
55572
  case "agent-init": {
@@ -28,9 +28,9 @@ to guess the customer's data model.
28
28
 
29
29
  ## Two ways to begin
30
30
 
31
- **A. Open the developer portal** — https://bo.wix.com/_api/himalaya-admin/admin-standalone/#/developer
32
- Browse the component reference, the importable SDKs, every guide, and a copy-paste prompt that
33
- bootstraps a coding agent. (Wix employees only — it sits on the corporate network.)
31
+ **A. Read the guides in your browser** — https://apps.wix.com/_api/himi-serve/docs
32
+ Every guide, the component reference and the importable SDKs, as readable pages. Public: no Wix
33
+ account, no corporate network. Append `/<topic>.md` for the raw markdown an agent should fetch.
34
34
 
35
35
  **B. From your terminal / coding agent:**
36
36
 
package/guides/index.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "componentCount": 82,
5
5
  "schemaSource": "core/ts/src/schema.ts"
6
6
  },
7
- "portalUrl": "https://bo.wix.com/_api/himalaya-admin/admin-standalone/#/developer",
7
+ "portalUrl": "https://apps.wix.com/_api/himi-serve/docs",
8
8
  "bootstrapPrompt": "I want to create a mobile app for my business. Follow this skill:\nhttps://apps.wix.com/_api/himi-serve/skill.md",
9
9
  "categories": [
10
10
  "Overview",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.911.0",
3
+ "version": "0.913.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -64,5 +64,5 @@
64
64
  "artifactId": "himalaya-cli"
65
65
  }
66
66
  },
67
- "falconPackageHash": "212114599a70158fe5cbefed85b7b784b0cd908f563d7061d4e87769"
67
+ "falconPackageHash": "74209736fe693e504b68490cef2bea93e7407a47b5ea88b4ba22e8ab"
68
68
  }
@@ -9,6 +9,23 @@ export function isOwnerPreviewLive(): boolean {
9
9
  return ownerPreviewMode() === "live";
10
10
  }
11
11
 
12
+ /**
13
+ * Whether we are in the web preview AT ALL — i.e. the host declared a mode.
14
+ *
15
+ * `isBridgeUnavailable` cannot answer this: it reads "not live" as "preview",
16
+ * and on NATIVE there is no input at all, so `undefined` satisfies it. That is
17
+ * harmless for the callers that use it to pick a demo fallback or to queue a
18
+ * write, and it is deliberately left alone for them — native write-behind
19
+ * depends on it. It is NOT harmless for choosing COPY: native workers format
20
+ * their failures the same way (`"HTTP \(code): \(body)"`, JSWorker.swift;
21
+ * `"HTTP ${e.code}: ..."`, WebViewJSWorker.kt), so a transient `HTTP 503` on a
22
+ * phone would otherwise be answered with "open this app on a device" — on the
23
+ * device. Callers that pick a SENTENCE ask this instead.
24
+ */
25
+ export function isOwnerPreviewHost(): boolean {
26
+ return ownerPreviewMode() !== undefined;
27
+ }
28
+
12
29
  export function isOwnerPreviewDemo(): boolean {
13
30
  return ownerPreviewMode() === "sample";
14
31
  }
@@ -31,6 +48,11 @@ export function shouldQueueOwnerWrite(e: unknown): boolean {
31
48
  const STATUS = /HTTP (\d{3})\b/;
32
49
  /** A wire body that is an HTML DOCUMENT, not a message. */
33
50
  const HTML_BODY = /<!DOCTYPE html|<html[\s>]|<head[\s>]/i;
51
+ /** A wire body that is a JSON ENVELOPE, not a message. The serve bridge answers
52
+ * its typed refusals this way — `HTTP 501: {"error":"webwix_owner_no_session",
53
+ * "app":"…"}` — and it is short enough to slip under MAX_RAW, so truncation
54
+ * never caught it and the owner read the machine's word for it. */
55
+ const JSON_BODY = /HTTP \d{3}:\s*[[{]/;
34
56
  /** Longest raw upstream text worth showing. Beyond this it stops being a
35
57
  * message and starts being a payload dump. */
36
58
  const MAX_RAW = 200;
@@ -62,16 +84,55 @@ function humanWireError(msg: string): string {
62
84
  return msg.length > MAX_RAW ? `${msg.slice(0, MAX_RAW).trimEnd()}…` : msg;
63
85
  }
64
86
 
65
- export function ownerLoadError(e: unknown): string {
87
+ /**
88
+ * The one sentence every owner screen gives for the DESIGNED bridge boundary.
89
+ *
90
+ * Copy-as-configuration (himi-copy): the key lives under the `ownerCommon.`
91
+ * prefix the dev module forwards to all owner screens, so one pack retitles it
92
+ * everywhere. With no pack this returns the stock copy verbatim.
93
+ */
94
+ export function ownerPreviewUnavailable(): string {
95
+ return copyParam(
96
+ "ownerCommon.bridgeUnavailable",
97
+ "Owner data isn’t available in the web preview — open this app on a device (▦ Open on phone) and sign in as the site owner.",
98
+ );
99
+ }
100
+
101
+ /**
102
+ * `ownerLoadError` for a screen that does nothing but RENDER the string.
103
+ *
104
+ * Two things the default cannot do, each for a reason:
105
+ *
106
+ * 1. It only names the web preview when there IS one. `isBridgeUnavailable`
107
+ * reads "not live" as "preview", and native injects no mode at all, so the
108
+ * default would answer a device-side `HTTP 503` with "open this app on a
109
+ * device" — on the device. That predicate is deliberately left alone:
110
+ * native write-behind (`shouldQueueOwnerWrite`) depends on exactly it.
111
+ *
112
+ * 2. It reduces a JSON ENVELOPE to its status. The default must NOT: its
113
+ * output is PARSED downstream — the products worker matches
114
+ * `CATALOG_V3_CALLING_CATALOG_V1_API` in it to choose its v1 fallback, and
115
+ * `owner-load-error.test.ts` pins that verbatim. A display-only caller has
116
+ * no such reader, and the bridge's typed refusals are JSON and short enough
117
+ * (68 chars) to slip under MAX_RAW, so nothing else was catching them.
118
+ *
119
+ * So: use this ONLY where the value is rendered and never matched on.
120
+ */
121
+ export function ownerDisplayError(e: unknown): string {
122
+ if (isOwnerPreviewHost() && isBridgeUnavailable(e)) return ownerPreviewUnavailable();
66
123
  const msg = String((e as Error)?.message ?? e);
67
- if (isBridgeUnavailable(e)) {
68
- // Copy-as-configuration (himi-copy): shared across every owner-lite worker,
69
- // so the key lives under the ownerCommon. prefix the dev module forwards to
70
- // all owner screens. With no pack this returns the stock copy verbatim.
124
+ const status = STATUS.exec(msg)?.[1];
125
+ if (JSON_BODY.test(msg) && status) {
71
126
  return copyParam(
72
- "ownerCommon.bridgeUnavailable",
73
- "Owner data isn’t available in the web preview — open this app on a device (▦ Open on phone) and sign in as the site owner.",
127
+ "ownerCommon.upstreamStatus",
128
+ `Wix returned an error (HTTP ${status}). Try again in a moment.`,
74
129
  );
75
130
  }
76
131
  return humanWireError(msg);
77
132
  }
133
+
134
+ export function ownerLoadError(e: unknown): string {
135
+ const msg = String((e as Error)?.message ?? e);
136
+ if (isBridgeUnavailable(e)) return ownerPreviewUnavailable();
137
+ return humanWireError(msg);
138
+ }
@@ -9,6 +9,23 @@ export function isOwnerPreviewLive(): boolean {
9
9
  return ownerPreviewMode() === "live";
10
10
  }
11
11
 
12
+ /**
13
+ * Whether we are in the web preview AT ALL — i.e. the host declared a mode.
14
+ *
15
+ * `isBridgeUnavailable` cannot answer this: it reads "not live" as "preview",
16
+ * and on NATIVE there is no input at all, so `undefined` satisfies it. That is
17
+ * harmless for the callers that use it to pick a demo fallback or to queue a
18
+ * write, and it is deliberately left alone for them — native write-behind
19
+ * depends on it. It is NOT harmless for choosing COPY: native workers format
20
+ * their failures the same way (`"HTTP \(code): \(body)"`, JSWorker.swift;
21
+ * `"HTTP ${e.code}: ..."`, WebViewJSWorker.kt), so a transient `HTTP 503` on a
22
+ * phone would otherwise be answered with "open this app on a device" — on the
23
+ * device. Callers that pick a SENTENCE ask this instead.
24
+ */
25
+ export function isOwnerPreviewHost(): boolean {
26
+ return ownerPreviewMode() !== undefined;
27
+ }
28
+
12
29
  export function isOwnerPreviewDemo(): boolean {
13
30
  return ownerPreviewMode() === "sample";
14
31
  }
@@ -31,6 +48,11 @@ export function shouldQueueOwnerWrite(e: unknown): boolean {
31
48
  const STATUS = /HTTP (\d{3})\b/;
32
49
  /** A wire body that is an HTML DOCUMENT, not a message. */
33
50
  const HTML_BODY = /<!DOCTYPE html|<html[\s>]|<head[\s>]/i;
51
+ /** A wire body that is a JSON ENVELOPE, not a message. The serve bridge answers
52
+ * its typed refusals this way — `HTTP 501: {"error":"webwix_owner_no_session",
53
+ * "app":"…"}` — and it is short enough to slip under MAX_RAW, so truncation
54
+ * never caught it and the owner read the machine's word for it. */
55
+ const JSON_BODY = /HTTP \d{3}:\s*[[{]/;
34
56
  /** Longest raw upstream text worth showing. Beyond this it stops being a
35
57
  * message and starts being a payload dump. */
36
58
  const MAX_RAW = 200;
@@ -62,16 +84,55 @@ function humanWireError(msg: string): string {
62
84
  return msg.length > MAX_RAW ? `${msg.slice(0, MAX_RAW).trimEnd()}…` : msg;
63
85
  }
64
86
 
65
- export function ownerLoadError(e: unknown): string {
87
+ /**
88
+ * The one sentence every owner screen gives for the DESIGNED bridge boundary.
89
+ *
90
+ * Copy-as-configuration (himi-copy): the key lives under the `ownerCommon.`
91
+ * prefix the dev module forwards to all owner screens, so one pack retitles it
92
+ * everywhere. With no pack this returns the stock copy verbatim.
93
+ */
94
+ export function ownerPreviewUnavailable(): string {
95
+ return copyParam(
96
+ "ownerCommon.bridgeUnavailable",
97
+ "Owner data isn’t available in the web preview — open this app on a device (▦ Open on phone) and sign in as the site owner.",
98
+ );
99
+ }
100
+
101
+ /**
102
+ * `ownerLoadError` for a screen that does nothing but RENDER the string.
103
+ *
104
+ * Two things the default cannot do, each for a reason:
105
+ *
106
+ * 1. It only names the web preview when there IS one. `isBridgeUnavailable`
107
+ * reads "not live" as "preview", and native injects no mode at all, so the
108
+ * default would answer a device-side `HTTP 503` with "open this app on a
109
+ * device" — on the device. That predicate is deliberately left alone:
110
+ * native write-behind (`shouldQueueOwnerWrite`) depends on exactly it.
111
+ *
112
+ * 2. It reduces a JSON ENVELOPE to its status. The default must NOT: its
113
+ * output is PARSED downstream — the products worker matches
114
+ * `CATALOG_V3_CALLING_CATALOG_V1_API` in it to choose its v1 fallback, and
115
+ * `owner-load-error.test.ts` pins that verbatim. A display-only caller has
116
+ * no such reader, and the bridge's typed refusals are JSON and short enough
117
+ * (68 chars) to slip under MAX_RAW, so nothing else was catching them.
118
+ *
119
+ * So: use this ONLY where the value is rendered and never matched on.
120
+ */
121
+ export function ownerDisplayError(e: unknown): string {
122
+ if (isOwnerPreviewHost() && isBridgeUnavailable(e)) return ownerPreviewUnavailable();
66
123
  const msg = String((e as Error)?.message ?? e);
67
- if (isBridgeUnavailable(e)) {
68
- // Copy-as-configuration (himi-copy): shared across every owner-lite worker,
69
- // so the key lives under the ownerCommon. prefix the dev module forwards to
70
- // all owner screens. With no pack this returns the stock copy verbatim.
124
+ const status = STATUS.exec(msg)?.[1];
125
+ if (JSON_BODY.test(msg) && status) {
71
126
  return copyParam(
72
- "ownerCommon.bridgeUnavailable",
73
- "Owner data isn’t available in the web preview — open this app on a device (▦ Open on phone) and sign in as the site owner.",
127
+ "ownerCommon.upstreamStatus",
128
+ `Wix returned an error (HTTP ${status}). Try again in a moment.`,
74
129
  );
75
130
  }
76
131
  return humanWireError(msg);
77
132
  }
133
+
134
+ export function ownerLoadError(e: unknown): string {
135
+ const msg = String((e as Error)?.message ?? e);
136
+ if (isBridgeUnavailable(e)) return ownerPreviewUnavailable();
137
+ return humanWireError(msg);
138
+ }
@@ -31,6 +31,7 @@ import { defineService } from "@himalaya/state";
31
31
  import { open } from "@himalaya/open-url";
32
32
  import { formatNumber } from "../_shared/engine-intl.js";
33
33
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
34
+ import { ownerDisplayError, isOwnerPreviewDemo, ownerPreviewUnavailable } from "../_shared/owner-load-error.js";
34
35
  import {
35
36
  getSiteAnalytics,
36
37
  getSiteTimeZone,
@@ -353,9 +354,41 @@ function withDetail(s: AnalyticsState, key: string): AnalyticsState {
353
354
  };
354
355
  }
355
356
 
357
+ /**
358
+ * The error arm, shaped once.
359
+ *
360
+ * `message` is ALWAYS human-readable copy, never a wire body. This screen used
361
+ * to pass `String(e.message)` straight through, which is how an owner opening
362
+ * the web preview met `HTTP 501: {"error":"webwix_bridge_no_client_id",…}` in
363
+ * the card where every sibling screen says the same thing in a sentence.
364
+ */
365
+ function errored(n: number, message: string): AnalyticsState {
366
+ return {
367
+ ...empty,
368
+ status: "error",
369
+ days: n,
370
+ rangeId: String(n),
371
+ range: rangeLabel(n),
372
+ notLoading: true,
373
+ notErrored: false,
374
+ hideContent: true,
375
+ noEmpty: true,
376
+ noCompare: true,
377
+ emptyText: "",
378
+ error: message,
379
+ handoffError: "",
380
+ };
381
+ }
382
+
356
383
  /** Fetch + shape the metrics for a window of `days`. */
357
384
  async function compute(days: number): Promise<AnalyticsState> {
358
385
  const n = RANGES.includes(days) ? days : 30;
386
+ // SAMPLE preview mode, and this screen deliberately has no sample arm (see the
387
+ // descriptor's "NO demo banner" note — fabricated traffic and revenue would be
388
+ // indistinguishable from the real thing). So refuse HERE, in one sentence,
389
+ // instead of firing a request the host has already told us cannot be served
390
+ // and rendering the bridge's refusal at the owner.
391
+ if (isOwnerPreviewDemo()) return errored(n, ownerPreviewUnavailable());
359
392
  try {
360
393
  // A site with no zone set resolves undefined and legitimately falls back to
361
394
  // UTC — the API's own default. A FAILED request rejects and lands in the
@@ -426,22 +459,7 @@ async function compute(days: number): Promise<AnalyticsState> {
426
459
  handoffError: "",
427
460
  };
428
461
  } catch (e) {
429
- const message = String((e as Error)?.message ?? e);
430
- return {
431
- ...empty,
432
- status: "error",
433
- days: n,
434
- rangeId: String(n),
435
- range: rangeLabel(n),
436
- notLoading: true,
437
- notErrored: false,
438
- hideContent: true,
439
- noEmpty: true,
440
- noCompare: true,
441
- emptyText: "",
442
- error: message,
443
- handoffError: "",
444
- };
462
+ return errored(n, ownerDisplayError(e));
445
463
  }
446
464
  }
447
465
 
@@ -46,6 +46,7 @@
46
46
  import { defineService } from "@himalaya/state";
47
47
  import { formatNumber } from "../_shared/engine-intl.js";
48
48
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
49
+ import { ownerDisplayError, isOwnerPreviewDemo, ownerPreviewUnavailable } from "../_shared/owner-load-error.js";
49
50
  import {
50
51
  getSiteTimeZone,
51
52
  intervalForDays,
@@ -472,7 +473,7 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
472
473
  return out;
473
474
  } catch (e) {
474
475
  if (mine !== seq) return latest ?? s;
475
- const out = errorState(s, String((e as Error)?.message ?? e));
476
+ const out = errorState(s, ownerDisplayError(e));
476
477
  latest = out;
477
478
  return out;
478
479
  }
@@ -494,6 +495,16 @@ const svc = defineService<ReportState, {
494
495
  */
495
496
  load: async (s, arg) => {
496
497
  stashCopy(arg);
498
+ // SAMPLE preview mode. Like the highlights service this screen has no
499
+ // sample arm on purpose — an invented subject area with invented rows is
500
+ // the "confident wrong number" this screen family exists to prevent — so
501
+ // it refuses in a sentence rather than issuing a call the host already
502
+ // knows cannot be served and surfacing the bridge's own refusal.
503
+ if (isOwnerPreviewDemo()) {
504
+ const out = errorState(s, ownerPreviewUnavailable());
505
+ latest = out;
506
+ return out;
507
+ }
497
508
  const mine = ++seq;
498
509
  try {
499
510
  const models = await listSemanticModels();
@@ -533,7 +544,7 @@ const svc = defineService<ReportState, {
533
544
  // made the failure path inconsistent with the success path right beside
534
545
  // it. (AI-review finding on #2533.)
535
546
  if (mine !== seq) return latest ?? s ?? { ...empty };
536
- const out = errorState(s, String((e as Error)?.message ?? e));
547
+ const out = errorState(s, ownerDisplayError(e));
537
548
  latest = out;
538
549
  return out;
539
550
  }
@@ -607,7 +618,7 @@ const svc = defineService<ReportState, {
607
618
  // Same hole, same reason: tap subject A then B, and A's rejection
608
619
  // arriving late must not replace B's selection with the error arm.
609
620
  if (mine !== seq) return latest ?? s;
610
- const out = errorState(s, String((e as Error)?.message ?? e));
621
+ const out = errorState(s, ownerDisplayError(e));
611
622
  latest = out;
612
623
  return out;
613
624
  } finally {
@@ -9,6 +9,23 @@ export function isOwnerPreviewLive(): boolean {
9
9
  return ownerPreviewMode() === "live";
10
10
  }
11
11
 
12
+ /**
13
+ * Whether we are in the web preview AT ALL — i.e. the host declared a mode.
14
+ *
15
+ * `isBridgeUnavailable` cannot answer this: it reads "not live" as "preview",
16
+ * and on NATIVE there is no input at all, so `undefined` satisfies it. That is
17
+ * harmless for the callers that use it to pick a demo fallback or to queue a
18
+ * write, and it is deliberately left alone for them — native write-behind
19
+ * depends on it. It is NOT harmless for choosing COPY: native workers format
20
+ * their failures the same way (`"HTTP \(code): \(body)"`, JSWorker.swift;
21
+ * `"HTTP ${e.code}: ..."`, WebViewJSWorker.kt), so a transient `HTTP 503` on a
22
+ * phone would otherwise be answered with "open this app on a device" — on the
23
+ * device. Callers that pick a SENTENCE ask this instead.
24
+ */
25
+ export function isOwnerPreviewHost(): boolean {
26
+ return ownerPreviewMode() !== undefined;
27
+ }
28
+
12
29
  export function isOwnerPreviewDemo(): boolean {
13
30
  return ownerPreviewMode() === "sample";
14
31
  }
@@ -31,6 +48,11 @@ export function shouldQueueOwnerWrite(e: unknown): boolean {
31
48
  const STATUS = /HTTP (\d{3})\b/;
32
49
  /** A wire body that is an HTML DOCUMENT, not a message. */
33
50
  const HTML_BODY = /<!DOCTYPE html|<html[\s>]|<head[\s>]/i;
51
+ /** A wire body that is a JSON ENVELOPE, not a message. The serve bridge answers
52
+ * its typed refusals this way — `HTTP 501: {"error":"webwix_owner_no_session",
53
+ * "app":"…"}` — and it is short enough to slip under MAX_RAW, so truncation
54
+ * never caught it and the owner read the machine's word for it. */
55
+ const JSON_BODY = /HTTP \d{3}:\s*[[{]/;
34
56
  /** Longest raw upstream text worth showing. Beyond this it stops being a
35
57
  * message and starts being a payload dump. */
36
58
  const MAX_RAW = 200;
@@ -62,16 +84,55 @@ function humanWireError(msg: string): string {
62
84
  return msg.length > MAX_RAW ? `${msg.slice(0, MAX_RAW).trimEnd()}…` : msg;
63
85
  }
64
86
 
65
- export function ownerLoadError(e: unknown): string {
87
+ /**
88
+ * The one sentence every owner screen gives for the DESIGNED bridge boundary.
89
+ *
90
+ * Copy-as-configuration (himi-copy): the key lives under the `ownerCommon.`
91
+ * prefix the dev module forwards to all owner screens, so one pack retitles it
92
+ * everywhere. With no pack this returns the stock copy verbatim.
93
+ */
94
+ export function ownerPreviewUnavailable(): string {
95
+ return copyParam(
96
+ "ownerCommon.bridgeUnavailable",
97
+ "Owner data isn’t available in the web preview — open this app on a device (▦ Open on phone) and sign in as the site owner.",
98
+ );
99
+ }
100
+
101
+ /**
102
+ * `ownerLoadError` for a screen that does nothing but RENDER the string.
103
+ *
104
+ * Two things the default cannot do, each for a reason:
105
+ *
106
+ * 1. It only names the web preview when there IS one. `isBridgeUnavailable`
107
+ * reads "not live" as "preview", and native injects no mode at all, so the
108
+ * default would answer a device-side `HTTP 503` with "open this app on a
109
+ * device" — on the device. That predicate is deliberately left alone:
110
+ * native write-behind (`shouldQueueOwnerWrite`) depends on exactly it.
111
+ *
112
+ * 2. It reduces a JSON ENVELOPE to its status. The default must NOT: its
113
+ * output is PARSED downstream — the products worker matches
114
+ * `CATALOG_V3_CALLING_CATALOG_V1_API` in it to choose its v1 fallback, and
115
+ * `owner-load-error.test.ts` pins that verbatim. A display-only caller has
116
+ * no such reader, and the bridge's typed refusals are JSON and short enough
117
+ * (68 chars) to slip under MAX_RAW, so nothing else was catching them.
118
+ *
119
+ * So: use this ONLY where the value is rendered and never matched on.
120
+ */
121
+ export function ownerDisplayError(e: unknown): string {
122
+ if (isOwnerPreviewHost() && isBridgeUnavailable(e)) return ownerPreviewUnavailable();
66
123
  const msg = String((e as Error)?.message ?? e);
67
- if (isBridgeUnavailable(e)) {
68
- // Copy-as-configuration (himi-copy): shared across every owner-lite worker,
69
- // so the key lives under the ownerCommon. prefix the dev module forwards to
70
- // all owner screens. With no pack this returns the stock copy verbatim.
124
+ const status = STATUS.exec(msg)?.[1];
125
+ if (JSON_BODY.test(msg) && status) {
71
126
  return copyParam(
72
- "ownerCommon.bridgeUnavailable",
73
- "Owner data isn’t available in the web preview — open this app on a device (▦ Open on phone) and sign in as the site owner.",
127
+ "ownerCommon.upstreamStatus",
128
+ `Wix returned an error (HTTP ${status}). Try again in a moment.`,
74
129
  );
75
130
  }
76
131
  return humanWireError(msg);
77
132
  }
133
+
134
+ export function ownerLoadError(e: unknown): string {
135
+ const msg = String((e as Error)?.message ?? e);
136
+ if (isBridgeUnavailable(e)) return ownerPreviewUnavailable();
137
+ return humanWireError(msg);
138
+ }
@@ -9,6 +9,23 @@ export function isOwnerPreviewLive(): boolean {
9
9
  return ownerPreviewMode() === "live";
10
10
  }
11
11
 
12
+ /**
13
+ * Whether we are in the web preview AT ALL — i.e. the host declared a mode.
14
+ *
15
+ * `isBridgeUnavailable` cannot answer this: it reads "not live" as "preview",
16
+ * and on NATIVE there is no input at all, so `undefined` satisfies it. That is
17
+ * harmless for the callers that use it to pick a demo fallback or to queue a
18
+ * write, and it is deliberately left alone for them — native write-behind
19
+ * depends on it. It is NOT harmless for choosing COPY: native workers format
20
+ * their failures the same way (`"HTTP \(code): \(body)"`, JSWorker.swift;
21
+ * `"HTTP ${e.code}: ..."`, WebViewJSWorker.kt), so a transient `HTTP 503` on a
22
+ * phone would otherwise be answered with "open this app on a device" — on the
23
+ * device. Callers that pick a SENTENCE ask this instead.
24
+ */
25
+ export function isOwnerPreviewHost(): boolean {
26
+ return ownerPreviewMode() !== undefined;
27
+ }
28
+
12
29
  export function isOwnerPreviewDemo(): boolean {
13
30
  return ownerPreviewMode() === "sample";
14
31
  }
@@ -31,6 +48,11 @@ export function shouldQueueOwnerWrite(e: unknown): boolean {
31
48
  const STATUS = /HTTP (\d{3})\b/;
32
49
  /** A wire body that is an HTML DOCUMENT, not a message. */
33
50
  const HTML_BODY = /<!DOCTYPE html|<html[\s>]|<head[\s>]/i;
51
+ /** A wire body that is a JSON ENVELOPE, not a message. The serve bridge answers
52
+ * its typed refusals this way — `HTTP 501: {"error":"webwix_owner_no_session",
53
+ * "app":"…"}` — and it is short enough to slip under MAX_RAW, so truncation
54
+ * never caught it and the owner read the machine's word for it. */
55
+ const JSON_BODY = /HTTP \d{3}:\s*[[{]/;
34
56
  /** Longest raw upstream text worth showing. Beyond this it stops being a
35
57
  * message and starts being a payload dump. */
36
58
  const MAX_RAW = 200;
@@ -62,16 +84,55 @@ function humanWireError(msg: string): string {
62
84
  return msg.length > MAX_RAW ? `${msg.slice(0, MAX_RAW).trimEnd()}…` : msg;
63
85
  }
64
86
 
65
- export function ownerLoadError(e: unknown): string {
87
+ /**
88
+ * The one sentence every owner screen gives for the DESIGNED bridge boundary.
89
+ *
90
+ * Copy-as-configuration (himi-copy): the key lives under the `ownerCommon.`
91
+ * prefix the dev module forwards to all owner screens, so one pack retitles it
92
+ * everywhere. With no pack this returns the stock copy verbatim.
93
+ */
94
+ export function ownerPreviewUnavailable(): string {
95
+ return copyParam(
96
+ "ownerCommon.bridgeUnavailable",
97
+ "Owner data isn’t available in the web preview — open this app on a device (▦ Open on phone) and sign in as the site owner.",
98
+ );
99
+ }
100
+
101
+ /**
102
+ * `ownerLoadError` for a screen that does nothing but RENDER the string.
103
+ *
104
+ * Two things the default cannot do, each for a reason:
105
+ *
106
+ * 1. It only names the web preview when there IS one. `isBridgeUnavailable`
107
+ * reads "not live" as "preview", and native injects no mode at all, so the
108
+ * default would answer a device-side `HTTP 503` with "open this app on a
109
+ * device" — on the device. That predicate is deliberately left alone:
110
+ * native write-behind (`shouldQueueOwnerWrite`) depends on exactly it.
111
+ *
112
+ * 2. It reduces a JSON ENVELOPE to its status. The default must NOT: its
113
+ * output is PARSED downstream — the products worker matches
114
+ * `CATALOG_V3_CALLING_CATALOG_V1_API` in it to choose its v1 fallback, and
115
+ * `owner-load-error.test.ts` pins that verbatim. A display-only caller has
116
+ * no such reader, and the bridge's typed refusals are JSON and short enough
117
+ * (68 chars) to slip under MAX_RAW, so nothing else was catching them.
118
+ *
119
+ * So: use this ONLY where the value is rendered and never matched on.
120
+ */
121
+ export function ownerDisplayError(e: unknown): string {
122
+ if (isOwnerPreviewHost() && isBridgeUnavailable(e)) return ownerPreviewUnavailable();
66
123
  const msg = String((e as Error)?.message ?? e);
67
- if (isBridgeUnavailable(e)) {
68
- // Copy-as-configuration (himi-copy): shared across every owner-lite worker,
69
- // so the key lives under the ownerCommon. prefix the dev module forwards to
70
- // all owner screens. With no pack this returns the stock copy verbatim.
124
+ const status = STATUS.exec(msg)?.[1];
125
+ if (JSON_BODY.test(msg) && status) {
71
126
  return copyParam(
72
- "ownerCommon.bridgeUnavailable",
73
- "Owner data isn’t available in the web preview — open this app on a device (▦ Open on phone) and sign in as the site owner.",
127
+ "ownerCommon.upstreamStatus",
128
+ `Wix returned an error (HTTP ${status}). Try again in a moment.`,
74
129
  );
75
130
  }
76
131
  return humanWireError(msg);
77
132
  }
133
+
134
+ export function ownerLoadError(e: unknown): string {
135
+ const msg = String((e as Error)?.message ?? e);
136
+ if (isBridgeUnavailable(e)) return ownerPreviewUnavailable();
137
+ return humanWireError(msg);
138
+ }