@geravant/sinain 1.22.1 → 1.22.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.22.1",
3
+ "version": "1.22.4",
4
4
  "description": "Ambient intelligence that sees what you see, hears what you hear, and acts on your behalf",
5
5
  "type": "module",
6
6
  "bin": {
@@ -896,30 +896,43 @@ async function renderEntityPage(entity) {
896
896
 
897
897
  // Auto-import path for share links — runs BEFORE the local existence check
898
898
  // so a recipient with no prior data on this entity gets the page populated.
899
+ // We KEEP the hash on failure so a refresh retries; only strip on success.
900
+ let shareError = null;
901
+ let shareMode = null;
899
902
  if (location.hash.startsWith("#bundle=")) {
903
+ shareMode = "bundle";
900
904
  try {
901
905
  const json = await ungzipBase64(location.hash.slice("#bundle=".length));
902
906
  await api("/knowledge/concepts/import?conflict=merge", {
903
907
  method: "POST", headers: {"Content-Type": "application/json"}, body: json
904
908
  });
905
909
  showToast("✓ Concept imported");
910
+ history.replaceState({}, "", location.pathname); // strip on success only
906
911
  } catch (e) {
907
- showToast("Import failed: " + (e.message || "decode error"));
912
+ shareError = (e && e.message) || "decode error";
908
913
  }
909
- history.replaceState({}, "", location.pathname); // strip hash
910
914
  } else if (location.hash.startsWith("#peer=")) {
915
+ shareMode = "peer";
911
916
  const token = location.hash.slice("#peer=".length);
912
- history.replaceState({}, "", location.pathname); // strip early — keeps refresh sane
913
917
  try {
914
918
  await ShareManager.connectAsRecipient(token);
915
919
  showToast("✓ Concept imported via peer");
920
+ history.replaceState({}, "", location.pathname); // strip on success only
916
921
  } catch (e) {
917
- showToast("Peer share failed: " + (e.message || "unreachable"));
922
+ shareError = (e && e.message) || "unreachable";
918
923
  }
919
924
  }
920
925
 
921
926
  const page = await api("/knowledge/page?entity=" + encodeURIComponent(entity));
922
927
  if (!page.ok || page.fact_count === 0) {
928
+ // If a share-import was attempted and failed AND we have no local data,
929
+ // render an explicit share-failed view rather than the generic
930
+ // MissingConcept page — the user needs to know the share didn't land,
931
+ // not just that the entity is absent.
932
+ if (shareError && page.fact_count === 0) {
933
+ renderShareFailed(entity, shareMode, shareError, root);
934
+ return;
935
+ }
923
936
  if (page.fact_count === 0) {
924
937
  renderMissingConcept(entity, root);
925
938
  return;
@@ -1184,6 +1197,50 @@ function showUndoToast(factId, undoToken, sourceEntity) {
1184
1197
  setTimeout(() => { if ($("#undoToast")) root.innerHTML = ""; }, ms);
1185
1198
  }
1186
1199
 
1200
+ // ── Share-failed landing ──────────────────────────────────────────────────
1201
+ // Shown when a #bundle= or #peer= share-link couldn't be imported AND no
1202
+ // local data exists for the entity. Distinct from MissingConcept because
1203
+ // the user explicitly tried to load shared data — they need to know that
1204
+ // transfer failed, not just that the entity isn't here.
1205
+ function renderShareFailed(entity, mode, errorMsg, root) {
1206
+ document.title = "Share couldn't load · Sinain";
1207
+ const isPeer = mode === "peer";
1208
+ root.innerHTML = \`
1209
+ <h1>Share couldn't be loaded</h1>
1210
+ <div class="error-block" style="margin-bottom: 16px;">
1211
+ <strong>\${esc(entity)}</strong> — \${isPeer ? "couldn't receive bundle from sender" : "couldn't decode bundle from URL"}.
1212
+ <br><br>
1213
+ <code>\${esc(errorMsg)}</code>
1214
+ </div>
1215
+ \${isPeer ? \`
1216
+ <p style="color: var(--fg-dim); line-height: 1.5;">
1217
+ Likely causes:
1218
+ </p>
1219
+ <ul style="color: var(--fg-dim); line-height: 1.6;">
1220
+ <li>The sender's tab is closed (peer share needs sender's sinain-core open).</li>
1221
+ <li>The share has expired or was revoked.</li>
1222
+ <li>Network/NAT blocked the WebRTC connection (about 10–20% of pairs need a TURN relay).</li>
1223
+ </ul>
1224
+ <p style="color: var(--fg-dim);">
1225
+ Ask the sender to reopen sinain (their peer registration auto-resumes), then click <strong>Retry</strong>.
1226
+ </p>\` : \`
1227
+ <p style="color: var(--fg-dim); line-height: 1.5;">
1228
+ The link's <code>#bundle=…</code> portion may have been truncated by a chat tool — some preview tools strip URL fragments.
1229
+ Ask the sender to re-share, ideally pasted as a code block so the URL stays intact.
1230
+ </p>\`}
1231
+ <div class="actions" style="margin-top: 20px; display: flex; gap: 10px;">
1232
+ <button class="primary" onclick="location.reload()">Retry</button>
1233
+ <button onclick="window._proceedWithoutShare('\${esc(entity)}')">Continue without share</button>
1234
+ </div>
1235
+ \`;
1236
+ // Continue-without-share clears the hash so the next render takes the
1237
+ // normal MissingConcept path (which has the file-drop dropzone).
1238
+ window._proceedWithoutShare = (eid) => {
1239
+ history.replaceState({}, "", "/knowledge/ui/entity/" + encodeURIComponent(eid));
1240
+ renderMissingConcept(eid, root);
1241
+ };
1242
+ }
1243
+
1187
1244
  // ── Missing concept landing ───────────────────────────────────────────────
1188
1245
  function renderMissingConcept(entity, root) {
1189
1246
  document.title = "Missing · " + entity;
@@ -1286,11 +1343,15 @@ function renderKnowledgeUiV2(): string {
1286
1343
  const peerHost = process.env.SINAIN_PEERJS_HOST || ""; // empty = peerjs.com cloud default
1287
1344
  const inlineMax = parseInt(process.env.SINAIN_SHARE_INLINE_MAX_BYTES || "6000");
1288
1345
  const ttlHours = parseInt(process.env.SINAIN_SHARE_TTL_HOURS || "24");
1289
- // Public URL of the share-redirector (docs/share.html in the repo). Browsers
1290
- // preserve URL fragments through redirects without sending them to the
1291
- // server, so the bundle bytes never touch this CDN.
1346
+ // Public URL of the share-redirector. Self-hosted on the existing
1347
+ // sinain.duckdns.org Caddy + Let's Encrypt setup that already fronts the
1348
+ // OpenClaw gateway. We control the host, headers, and reliability no
1349
+ // CDN policy quirks (jsDelivr serves gh-path HTML as text/plain;
1350
+ // raw.githack.com returns 403 with body). Browsers preserve URL fragments
1351
+ // through redirects without sending them to the server, so bundle bytes
1352
+ // in #bundle=… never touch our host either.
1292
1353
  const shareBaseUrl = process.env.SINAIN_SHARE_BASE_URL
1293
- || "https://cdn.jsdelivr.net/gh/anthillnet/sinain-hud@main/docs/share.html";
1354
+ || "https://sinain.duckdns.org/share.html";
1294
1355
  return KNOWLEDGE_UI_V2_HTML
1295
1356
  .replace(/__SHARE_PEERJS_HOST__/g, JSON.stringify(peerHost))
1296
1357
  .replace(/__SHARE_INLINE_MAX_BYTES__/g, String(inlineMax))