dexterm-cli 0.0.0-stage → 0.1.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/LICENSE +661 -0
- package/dist/packages/agents/src/audit.js +122 -0
- package/dist/packages/agents/src/bus.js +379 -0
- package/dist/packages/agents/src/executor.js +177 -0
- package/dist/packages/agents/src/gate.js +176 -0
- package/dist/packages/agents/src/identity.js +179 -0
- package/dist/packages/agents/src/index.js +15 -0
- package/dist/packages/agents/src/proposals.js +153 -0
- package/dist/packages/agents/src/schema.js +87 -0
- package/dist/packages/agents/src/sdk.js +140 -0
- package/dist/packages/agents/src/tiers.js +114 -0
- package/dist/packages/agents/src/tools.js +69 -0
- package/dist/packages/agents/src/ws.js +158 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
- package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +67 -0
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +64 -0
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
- package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
- package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
- package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
- package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
- package/dist/packages/cli/bin/dexterm.mjs +11 -0
- package/dist/packages/cli/src/agent-cmd.js +303 -0
- package/dist/packages/cli/src/agent-roster.js +26 -0
- package/dist/packages/cli/src/analyst/backend.js +526 -0
- package/dist/packages/cli/src/analyst/results.js +38 -0
- package/dist/packages/cli/src/analyst/session.js +319 -0
- package/dist/packages/cli/src/audit.js +51 -0
- package/dist/packages/cli/src/backtest-cmd.js +149 -0
- package/dist/packages/cli/src/boot-state.js +62 -0
- package/dist/packages/cli/src/custom.js +261 -0
- package/dist/packages/cli/src/display.js +191 -0
- package/dist/packages/cli/src/doctor.js +361 -0
- package/dist/packages/cli/src/feed.js +634 -0
- package/dist/packages/cli/src/fixtures.js +275 -0
- package/dist/packages/cli/src/follow-cmd.js +221 -0
- package/dist/packages/cli/src/images-config.js +59 -0
- package/dist/packages/cli/src/index.js +31 -0
- package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
- package/dist/packages/cli/src/larp-record.js +21 -0
- package/dist/packages/cli/src/live/copy.js +80 -0
- package/dist/packages/cli/src/live/flow.js +233 -0
- package/dist/packages/cli/src/live/index.js +509 -0
- package/dist/packages/cli/src/live/intel.js +227 -0
- package/dist/packages/cli/src/live/newpairs.js +334 -0
- package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
- package/dist/packages/cli/src/live/screens/token-live.js +459 -0
- package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
- package/dist/packages/cli/src/live/token.js +585 -0
- package/dist/packages/cli/src/live/trade.js +929 -0
- package/dist/packages/cli/src/live/watchlist.js +178 -0
- package/dist/packages/cli/src/main.js +5583 -0
- package/dist/packages/cli/src/narrative-cmd.js +76 -0
- package/dist/packages/cli/src/pack-cmd.js +323 -0
- package/dist/packages/cli/src/plan-cmd.js +278 -0
- package/dist/packages/cli/src/popup.js +810 -0
- package/dist/packages/cli/src/rules-cmd.js +131 -0
- package/dist/packages/cli/src/rules.js +339 -0
- package/dist/packages/cli/src/scan/command.js +57 -0
- package/dist/packages/cli/src/scan/engine.js +419 -0
- package/dist/packages/cli/src/scan/observe.js +64 -0
- package/dist/packages/cli/src/scan/ui.js +98 -0
- package/dist/packages/cli/src/screener/filter.js +241 -0
- package/dist/packages/cli/src/screener/presets.js +80 -0
- package/dist/packages/cli/src/screener/rows.js +146 -0
- package/dist/packages/cli/src/screener/rug.js +50 -0
- package/dist/packages/cli/src/screener/sort.js +52 -0
- package/dist/packages/cli/src/screener/view.js +183 -0
- package/dist/packages/cli/src/screens/analyst-session.js +343 -0
- package/dist/packages/cli/src/screens/analyst.js +326 -0
- package/dist/packages/cli/src/screens/boot.js +74 -0
- package/dist/packages/cli/src/screens/bubbles.js +255 -0
- package/dist/packages/cli/src/screens/coin.js +33 -0
- package/dist/packages/cli/src/screens/follows.js +201 -0
- package/dist/packages/cli/src/screens/kol.js +90 -0
- package/dist/packages/cli/src/screens/narrative.js +90 -0
- package/dist/packages/cli/src/screens/newpairs.js +150 -0
- package/dist/packages/cli/src/screens/overlay.js +257 -0
- package/dist/packages/cli/src/screens/plans.js +126 -0
- package/dist/packages/cli/src/screens/positions.js +176 -0
- package/dist/packages/cli/src/screens/rules.js +170 -0
- package/dist/packages/cli/src/screens/screener.js +459 -0
- package/dist/packages/cli/src/screens/social-panel.js +107 -0
- package/dist/packages/cli/src/screens/ticket.js +342 -0
- package/dist/packages/cli/src/screens/token.js +312 -0
- package/dist/packages/cli/src/screens/trending.js +181 -0
- package/dist/packages/cli/src/screens/watchlist.js +104 -0
- package/dist/packages/cli/src/social/demo.js +96 -0
- package/dist/packages/cli/src/social/view.js +250 -0
- package/dist/packages/cli/src/state.js +463 -0
- package/dist/packages/cli/src/theme-cmd.js +126 -0
- package/dist/packages/cli/src/trade.js +319 -0
- package/dist/packages/cli/src/voice-hud.js +42 -0
- package/dist/packages/cli/src/wallet-cmd.js +440 -0
- package/dist/packages/cli/src/wallet-command.js +31 -0
- package/dist/packages/cli/src/widgets.js +208 -0
- package/dist/packages/cli/src/workspace-cmd.js +68 -0
- package/dist/packages/cli/src/x-cmd.js +154 -0
- package/dist/packages/custom/src/config.js +213 -0
- package/dist/packages/custom/src/index.js +10 -0
- package/dist/packages/custom/src/keymap.js +107 -0
- package/dist/packages/custom/src/layout.js +235 -0
- package/dist/packages/custom/src/packs.js +633 -0
- package/dist/packages/custom/src/rules.js +704 -0
- package/dist/packages/custom/src/schema.js +515 -0
- package/dist/packages/custom/src/themes.js +176 -0
- package/dist/packages/custom/src/widget-html.js +222 -0
- package/dist/packages/custom/src/workspace.js +141 -0
- package/dist/packages/data/src/analyst-actions.js +133 -0
- package/dist/packages/data/src/budget.js +88 -0
- package/dist/packages/data/src/cache.js +142 -0
- package/dist/packages/data/src/candles.js +168 -0
- package/dist/packages/data/src/clock.js +64 -0
- package/dist/packages/data/src/credits.js +137 -0
- package/dist/packages/data/src/detail.js +97 -0
- package/dist/packages/data/src/fast-lane.js +321 -0
- package/dist/packages/data/src/fixtures.js +135 -0
- package/dist/packages/data/src/health.js +245 -0
- package/dist/packages/data/src/http.js +277 -0
- package/dist/packages/data/src/images/cache.js +141 -0
- package/dist/packages/data/src/images/decode-worker.js +28 -0
- package/dist/packages/data/src/images/decode.js +81 -0
- package/dist/packages/data/src/images/fetch.js +162 -0
- package/dist/packages/data/src/images/gif.js +214 -0
- package/dist/packages/data/src/images/index.js +10 -0
- package/dist/packages/data/src/images/jpeg.js +544 -0
- package/dist/packages/data/src/images/png.js +310 -0
- package/dist/packages/data/src/images/service.js +334 -0
- package/dist/packages/data/src/images/types.js +27 -0
- package/dist/packages/data/src/index.js +208 -0
- package/dist/packages/data/src/intel/helius-history.js +59 -0
- package/dist/packages/data/src/intel/holder-class.js +125 -0
- package/dist/packages/data/src/intel/rules.js +421 -0
- package/dist/packages/data/src/intel/scan.js +170 -0
- package/dist/packages/data/src/larp/evidence.js +288 -0
- package/dist/packages/data/src/larp/links.js +137 -0
- package/dist/packages/data/src/larp/robots.js +90 -0
- package/dist/packages/data/src/larp/safe-fetch.js +389 -0
- package/dist/packages/data/src/limiter.js +341 -0
- package/dist/packages/data/src/model.js +12 -0
- package/dist/packages/data/src/paper.js +331 -0
- package/dist/packages/data/src/rpc-config.js +126 -0
- package/dist/packages/data/src/rpc-pool.js +166 -0
- package/dist/packages/data/src/social/chain.js +83 -0
- package/dist/packages/data/src/social/dedupe.js +205 -0
- package/dist/packages/data/src/social/feed.js +462 -0
- package/dist/packages/data/src/social/firewall.js +110 -0
- package/dist/packages/data/src/social/index.js +11 -0
- package/dist/packages/data/src/social/kol.js +269 -0
- package/dist/packages/data/src/social/model.js +11 -0
- package/dist/packages/data/src/social/rank.js +184 -0
- package/dist/packages/data/src/social/rss.js +113 -0
- package/dist/packages/data/src/social/summary.js +104 -0
- package/dist/packages/data/src/social/venues.js +186 -0
- package/dist/packages/data/src/sources/dexscreener.js +170 -0
- package/dist/packages/data/src/sources/geckoterminal.js +133 -0
- package/dist/packages/data/src/sources/goplus.js +161 -0
- package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
- package/dist/packages/data/src/sources/jupiter.js +198 -0
- package/dist/packages/data/src/sources/pda.js +113 -0
- package/dist/packages/data/src/sources/pump-curve.js +58 -0
- package/dist/packages/data/src/sources/pumpportal.js +247 -0
- package/dist/packages/data/src/sources/rugcheck.js +71 -0
- package/dist/packages/data/src/sources/solana-rpc.js +299 -0
- package/dist/packages/data/src/sources/solana-ws.js +287 -0
- package/dist/packages/data/src/store.js +289 -0
- package/dist/packages/data/src/trade/bridge.js +464 -0
- package/dist/packages/data/src/trade/caps.js +323 -0
- package/dist/packages/data/src/trade/copy/auto.js +385 -0
- package/dist/packages/data/src/trade/copy/follows.js +103 -0
- package/dist/packages/data/src/trade/copy/index.js +9 -0
- package/dist/packages/data/src/trade/copy/mirror.js +91 -0
- package/dist/packages/data/src/trade/copy/stats.js +120 -0
- package/dist/packages/data/src/trade/copy/tracker.js +158 -0
- package/dist/packages/data/src/trade/copy/trades.js +87 -0
- package/dist/packages/data/src/trade/copy/types.js +6 -0
- package/dist/packages/data/src/trade/executor.js +933 -0
- package/dist/packages/data/src/trade/kill-switch.js +47 -0
- package/dist/packages/data/src/trade/link-store.js +52 -0
- package/dist/packages/data/src/trade/plans/confirm.js +157 -0
- package/dist/packages/data/src/trade/plans/engine.js +784 -0
- package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
- package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
- package/dist/packages/data/src/trade/plans/executor.js +11 -0
- package/dist/packages/data/src/trade/plans/index.js +12 -0
- package/dist/packages/data/src/trade/plans/journal.js +77 -0
- package/dist/packages/data/src/trade/plans/mandate.js +129 -0
- package/dist/packages/data/src/trade/plans/plan.js +179 -0
- package/dist/packages/data/src/trade/plans/service.js +168 -0
- package/dist/packages/data/src/trade/plans/types.js +6 -0
- package/dist/packages/data/src/trade/rpc-exec.js +229 -0
- package/dist/packages/data/src/trade/signer/client.js +197 -0
- package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
- package/dist/packages/data/src/trade/signer/host.js +97 -0
- package/dist/packages/data/src/trade/signer/main.js +121 -0
- package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
- package/dist/packages/data/src/trade/signer/protocol.js +92 -0
- package/dist/packages/data/src/trade/signer/service.js +400 -0
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
- package/dist/packages/data/src/trade/sol-usd.js +86 -0
- package/dist/packages/data/src/trade/swap-build.js +108 -0
- package/dist/packages/data/src/trade/token-delta.js +201 -0
- package/dist/packages/data/src/trade/tx-decode.js +632 -0
- package/dist/packages/data/src/util.js +24 -0
- package/dist/packages/data/src/watchlist.js +176 -0
- package/dist/packages/data/src/x/api.js +256 -0
- package/dist/packages/data/src/x/budget.js +66 -0
- package/dist/packages/data/src/x/config.js +49 -0
- package/dist/packages/data/src/x/index.js +10 -0
- package/dist/packages/data/src/x/mock-server.js +217 -0
- package/dist/packages/data/src/x/oauth.js +242 -0
- package/dist/packages/data/src/x/pkce.js +21 -0
- package/dist/packages/data/src/x/source.js +285 -0
- package/dist/packages/data/src/x/token-store.js +134 -0
- package/dist/packages/data/src/x/transport.js +71 -0
- package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
- package/dist/packages/mcp/assets/cases.json +1194 -0
- package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
- package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
- package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
- package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
- package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
- package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
- package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
- package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
- package/dist/packages/mcp/src/dossier.js +78 -0
- package/dist/packages/mcp/src/firewall.js +21 -0
- package/dist/packages/mcp/src/index.js +22 -0
- package/dist/packages/mcp/src/server.js +849 -0
- package/dist/packages/mcp/src/user-content.js +434 -0
- package/dist/packages/packs/src/backtest.js +183 -0
- package/dist/packages/packs/src/digest.js +95 -0
- package/dist/packages/packs/src/firewall.js +57 -0
- package/dist/packages/packs/src/index.js +19 -0
- package/dist/packages/packs/src/larp-eval.js +100 -0
- package/dist/packages/packs/src/narrative.js +309 -0
- package/dist/packages/packs/src/reason.js +135 -0
- package/dist/packages/packs/src/rug-model.js +137 -0
- package/dist/packages/packs/src/schema.js +54 -0
- package/dist/packages/packs/src/token-dossier.js +174 -0
- package/dist/packages/packs/src/verdicts.js +17 -0
- package/dist/packages/packs/src/wallet-dossier.js +105 -0
- package/dist/packages/popup/src/config.js +71 -0
- package/dist/packages/popup/src/dock.js +271 -0
- package/dist/packages/popup/src/host.js +210 -0
- package/dist/packages/popup/src/index.js +12 -0
- package/dist/packages/popup/src/loopback.js +53 -0
- package/dist/packages/popup/src/open.js +183 -0
- package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
- package/dist/packages/popup/src/pages/bubbles.css +23 -0
- package/dist/packages/popup/src/pages/bubbles.html +36 -0
- package/dist/packages/popup/src/pages/bubbles.js +214 -0
- package/dist/packages/popup/src/pages/chart-lib.js +262 -0
- package/dist/packages/popup/src/pages/chart.css +50 -0
- package/dist/packages/popup/src/pages/chart.html +56 -0
- package/dist/packages/popup/src/pages/chart.js +554 -0
- package/dist/packages/popup/src/pages/common.css +34 -0
- package/dist/packages/popup/src/pages/common.js +187 -0
- package/dist/packages/popup/src/pages/feeds.html +20 -0
- package/dist/packages/popup/src/pages/feeds.js +77 -0
- package/dist/packages/popup/src/pages/narrative.html +20 -0
- package/dist/packages/popup/src/pages/narrative.js +61 -0
- package/dist/packages/popup/src/pages/plans.css +30 -0
- package/dist/packages/popup/src/pages/plans.html +26 -0
- package/dist/packages/popup/src/pages/plans.js +82 -0
- package/dist/packages/popup/src/pages/roster.css +41 -0
- package/dist/packages/popup/src/pages/roster.html +25 -0
- package/dist/packages/popup/src/pages/roster.js +75 -0
- package/dist/packages/popup/src/pages/token.html +20 -0
- package/dist/packages/popup/src/pages/token.js +112 -0
- package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
- package/dist/packages/popup/src/pages/widget.css +5 -0
- package/dist/packages/popup/src/pages/widget.html +21 -0
- package/dist/packages/popup/src/pages/widget.js +85 -0
- package/dist/packages/popup/src/protocol.js +31 -0
- package/dist/packages/popup/src/selftest.js +156 -0
- package/dist/packages/popup/src/server.js +582 -0
- package/dist/packages/popup/src/sinkproxy.js +167 -0
- package/dist/packages/popup/src/widget-html.js +231 -0
- package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
- package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
- package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
- package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
- package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
- package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
- package/dist/packages/recorder/src/cli.js +113 -0
- package/dist/packages/recorder/src/export.js +71 -0
- package/dist/packages/recorder/src/index.js +20 -0
- package/dist/packages/recorder/src/labels.js +151 -0
- package/dist/packages/recorder/src/open.js +29 -0
- package/dist/packages/recorder/src/recorder.js +553 -0
- package/dist/packages/recorder/src/schedule.js +43 -0
- package/dist/packages/recorder/src/schema.js +135 -0
- package/dist/packages/recorder/src/snapshot.js +191 -0
- package/dist/packages/recorder/src/sweep.js +52 -0
- package/dist/packages/recorder/src/verdicts.js +52 -0
- package/dist/packages/tui/src/bubbles.js +415 -0
- package/dist/packages/tui/src/canvas.js +370 -0
- package/dist/packages/tui/src/chrome.js +85 -0
- package/dist/packages/tui/src/color.js +133 -0
- package/dist/packages/tui/src/flush.js +62 -0
- package/dist/packages/tui/src/fmt.js +188 -0
- package/dist/packages/tui/src/fx-trade.js +278 -0
- package/dist/packages/tui/src/fx.js +527 -0
- package/dist/packages/tui/src/images/blocks.js +141 -0
- package/dist/packages/tui/src/images/coin.js +22 -0
- package/dist/packages/tui/src/images/detect.js +140 -0
- package/dist/packages/tui/src/images/index.js +7 -0
- package/dist/packages/tui/src/images/layer.js +146 -0
- package/dist/packages/tui/src/images/pixels.js +157 -0
- package/dist/packages/tui/src/images/protocols.js +87 -0
- package/dist/packages/tui/src/index.js +15 -0
- package/dist/packages/tui/src/keymap.js +355 -0
- package/dist/packages/tui/src/layout.js +11 -0
- package/dist/packages/tui/src/sound.js +177 -0
- package/dist/packages/tui/src/theme.js +182 -0
- package/dist/packages/tui/src/widgets.js +598 -0
- package/dist/packages/voice/src/controller.js +395 -0
- package/dist/packages/voice/src/doctor.js +63 -0
- package/dist/packages/voice/src/engines.js +44 -0
- package/dist/packages/voice/src/index.js +16 -0
- package/dist/packages/voice/src/router.js +502 -0
- package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
- package/dist/packages/voice/src/stt/apple.js +179 -0
- package/dist/packages/voice/src/stt/fake.js +34 -0
- package/dist/packages/voice/src/stt/whisper.js +102 -0
- package/dist/packages/voice/src/summary.js +44 -0
- package/dist/packages/voice/src/tts/fake.js +30 -0
- package/dist/packages/voice/src/tts/say.js +96 -0
- package/dist/packages/voice/src/types.js +3 -0
- package/package.json +42 -4
- package/README.md +0 -3
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// dexterm popup host — the small, lane-reusable facade (task-1290 scope item 3).
|
|
2
|
+
//
|
|
3
|
+
// const mgr = createPopupManager({ theme, onEvent });
|
|
4
|
+
// const h = await mgr.open({ kind: 'chart', title: 'WIF', data: chartVm }); // opens a frameless HUD window
|
|
5
|
+
// mgr.setData(h.id, nextChartVm); // the popup follows the open coin
|
|
6
|
+
// h.close(); // closing never touches the TUI
|
|
7
|
+
//
|
|
8
|
+
// The same `open({ kind, url|route, title })` is what Jarvis will call ("show me the bubble map for X") and
|
|
9
|
+
// what the trading lane's wallet bridge can call to render its loopback page in a chromeless window instead
|
|
10
|
+
// of the default browser — pass `url` for a custom loopback page, or `route` for one of the built-in kinds.
|
|
11
|
+
import { EventEmitter } from 'node:events';
|
|
12
|
+
import { randomBytes } from 'node:crypto';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import { createPopupServer } from "./server.js";
|
|
15
|
+
import { createPopupHost } from "./host.js";
|
|
16
|
+
import { createSinkProxy } from "./sinkproxy.js";
|
|
17
|
+
import { DEFAULT_HOTKEY, DEFAULT_RAIN } from "./config.js";
|
|
18
|
+
/** macOS major (Darwin kernel major − 9): WKWebsiteDataStore.proxyConfigurations needs macOS 14 = Darwin 23. */
|
|
19
|
+
const darwinMajor = () => Number(os.release().split('.')[0]) || 0;
|
|
20
|
+
// A custom popup URL may only point at our own loopback server origin (http://127.0.0.1:<server-port>/…).
|
|
21
|
+
// This enforces the ROADMAP "no remote content" rule in code, not just in the doc comment: https, any
|
|
22
|
+
// other host, any other port, userinfo and non-http schemes are all rejected before a window or browser
|
|
23
|
+
// tab opens. The rule itself lives in ./loopback.ts (shared with the browser fallback); re-exported here.
|
|
24
|
+
import { assertLoopbackUrl } from "./loopback.js";
|
|
25
|
+
export { assertLoopbackUrl, loopbackUrlProblem } from "./loopback.js";
|
|
26
|
+
export function createPopupManager(opts = {}) {
|
|
27
|
+
const emitter = new EventEmitter();
|
|
28
|
+
if (opts.onEvent)
|
|
29
|
+
emitter.on('event', opts.onEvent);
|
|
30
|
+
let server = null;
|
|
31
|
+
let host = null;
|
|
32
|
+
let native = false;
|
|
33
|
+
let reason = 'not started';
|
|
34
|
+
let hotkeyRegistered = false;
|
|
35
|
+
let hostDead = false;
|
|
36
|
+
let startPromise = null;
|
|
37
|
+
const openIds = new Set();
|
|
38
|
+
// task-1358 r4: the sink proxy + the widget gate (see PopupManager.widgetsAllowed)
|
|
39
|
+
let sinkProxy = null;
|
|
40
|
+
let widgetsOk = false;
|
|
41
|
+
let widgetReason = 'popups not started';
|
|
42
|
+
const warn = (m) => { opts.onWarn?.(m); };
|
|
43
|
+
const noWidgets = (why) => { widgetsOk = false; widgetReason = why; };
|
|
44
|
+
/** Swap a crashed native shell for the browser fallback so the next open() degrades instead of throwing. */
|
|
45
|
+
async function switchToBrowser(why) {
|
|
46
|
+
hostDead = false;
|
|
47
|
+
native = false;
|
|
48
|
+
hotkeyRegistered = false;
|
|
49
|
+
reason = `${why}; using browser`;
|
|
50
|
+
noWidgets(`browser fallback (${reason})`);
|
|
51
|
+
const fb = createPopupHost({ forceBrowser: true, env: opts.env, platform: opts.platform, port: server?.port });
|
|
52
|
+
host = fb.host;
|
|
53
|
+
await host.start();
|
|
54
|
+
warn(`popups open in your browser: ${reason}`);
|
|
55
|
+
}
|
|
56
|
+
async function ensureStarted() {
|
|
57
|
+
if (startPromise)
|
|
58
|
+
return startPromise;
|
|
59
|
+
startPromise = (async () => {
|
|
60
|
+
server = await createPopupServer({
|
|
61
|
+
theme: opts.theme,
|
|
62
|
+
rain: opts.rain ?? { ...DEFAULT_RAIN },
|
|
63
|
+
onEvent: (ev) => emitter.emit('event', ev),
|
|
64
|
+
...(opts.imageProvider ? { imageProvider: opts.imageProvider } : {}),
|
|
65
|
+
});
|
|
66
|
+
const hostOpts = { hotkey: opts.hotkey ?? DEFAULT_HOTKEY, hotkeyClose: opts.hotkeyClose, forceBrowser: opts.forceBrowser, env: opts.env, platform: opts.platform, port: server.port };
|
|
67
|
+
let decision = createPopupHost(hostOpts);
|
|
68
|
+
if (decision.native) {
|
|
69
|
+
// r4: the shell gets a loopback sink proxy that tunnels to the popup server and nowhere else; every
|
|
70
|
+
// connection a WebView makes — page loads, the socket, a runtime <link rel=preconnect> — goes through it.
|
|
71
|
+
const srvPort = server.port;
|
|
72
|
+
sinkProxy = await createSinkProxy({ targetPort: srvPort, onRefused: (r) => emitter.emit('event', { ev: 'proxy.refused', ...r }) });
|
|
73
|
+
decision = createPopupHost({ ...hostOpts, proxyPort: sinkProxy.port });
|
|
74
|
+
}
|
|
75
|
+
host = decision.host;
|
|
76
|
+
native = decision.native;
|
|
77
|
+
reason = decision.reason;
|
|
78
|
+
if (!native) {
|
|
79
|
+
warn(`popups open in your browser: ${reason}`);
|
|
80
|
+
noWidgets(`browser fallback (${reason})`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// If the native window shell dies (crash/kill), mark it dead so the next open() re-starts via the
|
|
84
|
+
// browser fallback instead of letting a later close/open throw "shell not running" into the TUI.
|
|
85
|
+
const nativeHost = host;
|
|
86
|
+
nativeHost.once?.('exit', () => { hostDead = true; noWidgets('the popup shell exited'); });
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const info = await host.start();
|
|
90
|
+
if (info)
|
|
91
|
+
hotkeyRegistered = info.hotkeyRegistered;
|
|
92
|
+
if (native) {
|
|
93
|
+
const confirmed = info?.sinkProxy ?? null;
|
|
94
|
+
if ((opts.platform ?? process.platform) === 'darwin' && darwinMajor() < 23)
|
|
95
|
+
noWidgets('the sink proxy needs macOS 14 or newer');
|
|
96
|
+
else if (!sinkProxy || confirmed !== sinkProxy.address)
|
|
97
|
+
noWidgets(`the popup shell did not confirm the sink proxy (${confirmed ?? 'none'})`);
|
|
98
|
+
else {
|
|
99
|
+
widgetsOk = true;
|
|
100
|
+
widgetReason = `native shell behind the sink proxy ${sinkProxy.address}`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
// The shell was present but would not start (missing libs, no GUI session, crash): degrade to the
|
|
106
|
+
// browser so a popup still opens, and tell the user why.
|
|
107
|
+
reason = `shell failed to start (${err.message}); using browser`;
|
|
108
|
+
native = false;
|
|
109
|
+
hotkeyRegistered = false;
|
|
110
|
+
noWidgets(`browser fallback (${reason})`);
|
|
111
|
+
warn(`popups open in your browser: ${reason}`);
|
|
112
|
+
const fb = createPopupHost({ forceBrowser: true, env: opts.env, platform: opts.platform, port: server.port });
|
|
113
|
+
host = fb.host;
|
|
114
|
+
await host.start();
|
|
115
|
+
}
|
|
116
|
+
})();
|
|
117
|
+
return startPromise;
|
|
118
|
+
}
|
|
119
|
+
function handleFor(id, kind) {
|
|
120
|
+
return {
|
|
121
|
+
id, kind, native,
|
|
122
|
+
setData: (data) => server?.setData(id, data),
|
|
123
|
+
sendData: (topic, payload) => server?.sendData(id, topic, payload),
|
|
124
|
+
close: () => { server?.closePopup(id); host?.close(id); openIds.delete(id); },
|
|
125
|
+
show: () => host?.show(id),
|
|
126
|
+
hide: () => host?.hide(id),
|
|
127
|
+
focus: () => host?.focus(id),
|
|
128
|
+
alwaysOnTop: (on) => host?.alwaysOnTop(id, on),
|
|
129
|
+
setBounds: (r) => { if (native && !hostDead)
|
|
130
|
+
host?.setBounds?.(id, r); },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
async open(req) {
|
|
135
|
+
await ensureStarted();
|
|
136
|
+
if (hostDead)
|
|
137
|
+
await switchToBrowser('popup window shell exited');
|
|
138
|
+
if (!server || !host)
|
|
139
|
+
throw new Error('popup host failed to start');
|
|
140
|
+
const id = `p_${randomBytes(6).toString('hex')}`;
|
|
141
|
+
const kind = req.route ?? req.kind;
|
|
142
|
+
// r4: a pack widget never opens in an engine the host does not route through its sink proxy
|
|
143
|
+
if ((kind === 'widget' || req.widget) && !widgetsOk)
|
|
144
|
+
throw new Error(`pack widgets open only in the native popup shell behind its sink proxy — ${widgetReason}`);
|
|
145
|
+
// A custom loopback URL (e.g. the wallet bridge's own page) is pinned to our own loopback origin —
|
|
146
|
+
// never a remote page; a built-in kind gets a single-use ticket URL from our server with the initial
|
|
147
|
+
// view model attached. assertLoopbackUrl throws on https/other-host/other-port before anything opens.
|
|
148
|
+
const url = req.url !== undefined
|
|
149
|
+
? assertLoopbackUrl(req.url, server.port)
|
|
150
|
+
: server.issueTicket(kind, id, { ...(req.data ? { data: req.data } : {}), ...(req.widget ? { widget: req.widget } : {}), ...(native ? {} : { fallback: reason }) });
|
|
151
|
+
host.open(id, url, req.geom);
|
|
152
|
+
openIds.add(id);
|
|
153
|
+
return handleFor(id, kind);
|
|
154
|
+
},
|
|
155
|
+
setData: (id, data) => server?.setData(id, data),
|
|
156
|
+
close: (id) => { server?.closePopup(id); host?.close(id); openIds.delete(id); },
|
|
157
|
+
get native() { return native; },
|
|
158
|
+
get reason() { return reason; },
|
|
159
|
+
get hotkeyRegistered() { return hotkeyRegistered; },
|
|
160
|
+
get port() { return server?.port ?? null; },
|
|
161
|
+
get widgetsAllowed() { return widgetsOk; },
|
|
162
|
+
get widgetReason() { return widgetReason; },
|
|
163
|
+
get sinkProxyPort() { return sinkProxy?.port ?? null; },
|
|
164
|
+
get sinkProxyStats() { return sinkProxy ? { forwarded: sinkProxy.forwarded, refused: sinkProxy.refused } : null; },
|
|
165
|
+
async shutdown() {
|
|
166
|
+
try {
|
|
167
|
+
host?.quit();
|
|
168
|
+
}
|
|
169
|
+
catch { /* ignore */ }
|
|
170
|
+
if (sinkProxy) {
|
|
171
|
+
await sinkProxy.close();
|
|
172
|
+
sinkProxy = null;
|
|
173
|
+
}
|
|
174
|
+
noWidgets('popups shut down');
|
|
175
|
+
if (server)
|
|
176
|
+
await server.close();
|
|
177
|
+
server = null;
|
|
178
|
+
host = null;
|
|
179
|
+
startPromise = null;
|
|
180
|
+
openIds.clear();
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// dexterm popup — bubble map pure helpers (task-1340). No DOM, no network, no globals besides the export.
|
|
2
|
+
// The SAME layout algorithm as @dexterm/tui bubbles.ts (packed circles per cluster, members radial around their
|
|
3
|
+
// hub, area ∝ share with a per-bubble cap), in plain JS so the page can inline it under its CSP nonce and
|
|
4
|
+
// test/bubbles-lib.test.ts can load it through node:vm. Pixel space here is real pixels (W × H), not half-rows.
|
|
5
|
+
(function (root) {
|
|
6
|
+
'use strict';
|
|
7
|
+
const KIND_LETTER = { 'dev-linked': 'D', dev: 'D', bundle: 'B', funder: 'F', transfer: 'T', sniper: 'S', pool: 'P', cex: 'X', whale: 'W', plain: '·' };
|
|
8
|
+
const LEGEND = [['dev', 'dev'], ['bundle', 'bundle'], ['funder', 'linked'], ['sniper', 'sniper'], ['pool', 'pool/LP'], ['cex', 'exchange'], ['plain', 'unlinked']];
|
|
9
|
+
|
|
10
|
+
/** Colour = rule; `t` is the popup theme tokens. */
|
|
11
|
+
function kindColor(t, kind) {
|
|
12
|
+
switch (kind) {
|
|
13
|
+
case 'dev': case 'dev-linked': return t.warn;
|
|
14
|
+
case 'bundle': return t.pump || t.accent;
|
|
15
|
+
case 'funder': return t.up;
|
|
16
|
+
case 'transfer': return t.dbc || t.warn;
|
|
17
|
+
case 'sniper': return t.down;
|
|
18
|
+
case 'pool': return t.accent;
|
|
19
|
+
case 'cex': case 'whale': return t.muted;
|
|
20
|
+
default: return t.dim;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function placeCluster(members) {
|
|
25
|
+
const hub = members[0];
|
|
26
|
+
const out = [{ i: hub.i, r: hub.r, dx: 0, dy: 0 }];
|
|
27
|
+
if (members.length === 1) return { items: out, R: hub.r + 1.5 };
|
|
28
|
+
let ringR = hub.r, k = 1, maxReach = hub.r;
|
|
29
|
+
while (k < members.length) {
|
|
30
|
+
const ring = [];
|
|
31
|
+
const d0 = ringR + 2;
|
|
32
|
+
let angleUsed = 0, biggest = 0;
|
|
33
|
+
while (k < members.length) {
|
|
34
|
+
const m = members[k];
|
|
35
|
+
const d = d0 + m.r;
|
|
36
|
+
const need = 2 * Math.asin(Math.min(1, (m.r + 1) / d));
|
|
37
|
+
if (ring.length && angleUsed + need > 2 * Math.PI) break;
|
|
38
|
+
ring.push({ m, d, need });
|
|
39
|
+
angleUsed += need; biggest = Math.max(biggest, m.r); k++;
|
|
40
|
+
}
|
|
41
|
+
const slack = Math.max(0, 2 * Math.PI - angleUsed) / ring.length;
|
|
42
|
+
let a = -Math.PI / 2 + (ring.length % 2 ? 0 : slack / 2) + out.length * 0.37;
|
|
43
|
+
for (const { m, d, need } of ring) {
|
|
44
|
+
a += need / 2 + slack / 2;
|
|
45
|
+
out.push({ i: m.i, r: m.r, dx: Math.cos(a) * d, dy: Math.sin(a) * d });
|
|
46
|
+
maxReach = Math.max(maxReach, d + m.r);
|
|
47
|
+
a += need / 2 + slack / 2;
|
|
48
|
+
}
|
|
49
|
+
ringR = d0 + 2 * biggest;
|
|
50
|
+
}
|
|
51
|
+
return { items: out, R: maxReach + 1.5 };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function packClusters(cls, W, H, gap) {
|
|
55
|
+
const placed = [];
|
|
56
|
+
const cx0 = W / 2, cy0 = H / 2;
|
|
57
|
+
const inside = (x, y, R) => x - R >= 0 && x + R <= W && y - R >= 0 && y + R <= H;
|
|
58
|
+
const free = (x, y, R) => placed.every((p) => Math.hypot(p.x - x, p.y - y) >= p.R + R + gap - 1e-6);
|
|
59
|
+
for (const c of cls) {
|
|
60
|
+
if (!placed.length) { if (!inside(cx0, cy0, c.R)) return null; placed.push(Object.assign({}, c, { x: cx0, y: cy0 })); continue; }
|
|
61
|
+
let best = null, bestD = Infinity;
|
|
62
|
+
for (const p of placed) {
|
|
63
|
+
for (let s = 0; s < 48; s++) {
|
|
64
|
+
const th = (s / 48) * 2 * Math.PI;
|
|
65
|
+
const d = p.R + c.R + gap;
|
|
66
|
+
const x = p.x + Math.cos(th) * d, y = p.y + Math.sin(th) * d;
|
|
67
|
+
if (!inside(x, y, c.R) || !free(x, y, c.R)) continue;
|
|
68
|
+
const dd = (x - cx0) ** 2 + ((y - cy0) * 1.15) ** 2;
|
|
69
|
+
if (dd < bestD) { bestD = dd; best = { x, y }; }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!best) return null;
|
|
73
|
+
placed.push(Object.assign({}, c, { x: best.x, y: best.y }));
|
|
74
|
+
}
|
|
75
|
+
return placed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Lay out holders in a W×H pixel area. Returns { bubbles:[{idx,cx,cy,r}], clusters:[{name,kind,pct,cx,cy,R,members,multi}], k }.
|
|
80
|
+
* `focus` = a cluster name → only that cluster (zoom). Deterministic. Never throws (grid fallback).
|
|
81
|
+
*/
|
|
82
|
+
function layout(holders, clusters, W, H, opts) {
|
|
83
|
+
opts = opts || {};
|
|
84
|
+
const idxs = holders.map((_, i) => i).filter((i) => !opts.focus || holders[i].cluster === opts.focus);
|
|
85
|
+
if (!idxs.length || W < 20 || H < 20) return { bubbles: [], clusters: [], k: 0, W, H };
|
|
86
|
+
const total = idxs.reduce((s, i) => s + Math.max(0.01, holders[i].pct), 0);
|
|
87
|
+
const fill = opts.fill != null ? opts.fill : opts.focus ? 0.6 : 0.5;
|
|
88
|
+
let k = Math.sqrt((fill * W * H) / (Math.PI * total));
|
|
89
|
+
const rMax = Math.min(W / 5.2, H / 2.6);
|
|
90
|
+
const gap = Math.max(2, Math.min(W, H) * 0.01);
|
|
91
|
+
for (let iter = 0; iter < 14; iter++) {
|
|
92
|
+
const byCluster = new Map();
|
|
93
|
+
for (const i of idxs) {
|
|
94
|
+
const h = holders[i];
|
|
95
|
+
const r = Math.max(3, Math.min(rMax, k * Math.sqrt(Math.max(0.01, h.pct))));
|
|
96
|
+
const key = h.cluster || `single:${i}`;
|
|
97
|
+
const arr = byCluster.get(key) || []; arr.push({ i, r }); byCluster.set(key, arr);
|
|
98
|
+
}
|
|
99
|
+
const cls = [];
|
|
100
|
+
for (const [key, members] of byCluster) {
|
|
101
|
+
members.sort((a, b) => b.r - a.r);
|
|
102
|
+
const pc = placeCluster(members);
|
|
103
|
+
const c = clusters.find((x) => x.name === key) || null;
|
|
104
|
+
cls.push({ key, name: c ? c.name : null, kind: c ? c.kind : holders[members[0].i].kind, pct: c ? c.pct : holders[members[0].i].pct, items: pc.items, R: pc.R, multi: members.length > 1 });
|
|
105
|
+
}
|
|
106
|
+
cls.sort((a, b) => b.R - a.R);
|
|
107
|
+
const placed = packClusters(cls, W, H, gap);
|
|
108
|
+
if (placed) {
|
|
109
|
+
const bubbles = [], out = [];
|
|
110
|
+
for (const p of placed) {
|
|
111
|
+
for (const it of p.items) bubbles.push({ idx: it.i, cx: p.x + it.dx, cy: p.y + it.dy, r: it.r });
|
|
112
|
+
out.push({ name: p.name, kind: p.kind, pct: p.pct, cx: p.x, cy: p.y, R: p.R, members: p.items.map((it) => it.i), multi: p.multi });
|
|
113
|
+
}
|
|
114
|
+
return { bubbles, clusters: out, k, W, H };
|
|
115
|
+
}
|
|
116
|
+
k *= 0.86;
|
|
117
|
+
}
|
|
118
|
+
const cols = Math.max(1, Math.floor(W / 24));
|
|
119
|
+
return { bubbles: idxs.map((i, n) => ({ idx: i, cx: 12 + (n % cols) * 24, cy: 12 + Math.floor(n / cols) * 24, r: 8 })), clusters: [], k, W, H, overflow: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Holder index under a point, or -1. */
|
|
123
|
+
function hit(lay, x, y) {
|
|
124
|
+
let best = -1, bestD = Infinity;
|
|
125
|
+
for (const b of lay.bubbles) { const d = Math.hypot(b.cx - x, b.cy - y); if (d <= b.r && d < bestD) { bestD = d; best = b.idx; } }
|
|
126
|
+
return best;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const shortAddr = (s, n) => { n = n || 4; s = String(s || ''); return s.length > 2 * n + 1 ? `${s.slice(0, n)}…${s.slice(-n)}` : s; };
|
|
130
|
+
const pctText = (p) => `${p.toFixed(p >= 10 ? 1 : 2)}%`;
|
|
131
|
+
const fmtNum = (n) => (n == null ? '—' : n >= 1e9 ? `${(n / 1e9).toFixed(2)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(2)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(Math.round(n)));
|
|
132
|
+
|
|
133
|
+
/** The rule panel lines for one holder: label · confidence, then the rule text, then up to 3 evidence lines. */
|
|
134
|
+
function whyLines(h, cluster) {
|
|
135
|
+
const lines = [];
|
|
136
|
+
if (!h) return lines;
|
|
137
|
+
lines.push({ kind: 'head', text: `${shortAddr(h.owner, 6)} ${pctText(h.pct)} ${h.kind.toUpperCase()}${h.cluster ? ` ${h.cluster}` : ''}` });
|
|
138
|
+
// task-1367: the holder class + its on-chain reason (absent from older terminals)
|
|
139
|
+
if (h.cls) {
|
|
140
|
+
lines.push({ kind: 'label', text: `■ ${h.cls.tag}`, right: 'on-chain', label: h.cls.tag === 'UNLABELLED' ? 'plain' : h.cls.tag === 'DEV' ? 'dev' : h.cls.tag === 'LINKED' ? 'dev-linked' : h.cls.tag === 'CEX' ? 'cex' : 'pool' });
|
|
141
|
+
lines.push({ kind: 'rule', text: h.cls.reason });
|
|
142
|
+
if (h.cls.lock) lines.push({ kind: 'muted', text: `${h.cls.lock.program} escrow ${shortAddr(h.cls.lock.escrow, 4)} · ${h.cls.lock.unlock || h.cls.lock.note}` });
|
|
143
|
+
}
|
|
144
|
+
if (h.funder) lines.push({ kind: 'muted', text: `funded by ${shortAddr(h.funder.address, 4)}${h.funder.label ? ` (${h.funder.label})` : ''}${h.funder.blockTime ? ` · ${new Date(h.funder.blockTime * 1000).toISOString().slice(0, 10)}` : ''}` });
|
|
145
|
+
else if (h.walletFirstSeen) lines.push({ kind: 'muted', text: `first seen ${new Date(h.walletFirstSeen * 1000).toISOString().slice(0, 10)} · funder not found in first txs` });
|
|
146
|
+
else lines.push({ kind: 'muted', text: 'funding not looked up (outside the funded sample)' });
|
|
147
|
+
const linked = cluster && cluster.members && cluster.members.length > 1;
|
|
148
|
+
const rules = h.labels && h.labels.length ? h.labels : linked
|
|
149
|
+
? [{ label: `linked · ${cluster.kind}`, rule: `no label of its own; linked into ${cluster.name} (${cluster.members.length} wallets, ${Number(cluster.pct).toFixed(1)}%) by the evidence below`, confidence: 'on-chain' }]
|
|
150
|
+
: [{ label: 'unlinked', rule: 'no rule matched: no shared funder, same-slot buy, transfer or creator link found in the sample', confidence: 'n/a' }];
|
|
151
|
+
for (const r of rules) { lines.push({ kind: 'label', text: `● ${r.label}`, right: r.confidence || '', label: r.label }); lines.push({ kind: 'rule', text: r.rule }); }
|
|
152
|
+
const ev = cluster ? cluster.evidence.filter((e) => e.linked !== false && (e.a === h.owner || e.b === h.owner)).slice(0, 3) : [];
|
|
153
|
+
for (const e of ev) lines.push({ kind: 'evidence', text: `↳ ${shortAddr(e.a, 4)} ↔ ${shortAddr(e.b, 4)}: ${e.rule}` });
|
|
154
|
+
lines.push({ kind: 'foot', text: 'labels are heuristics (rule + evidence) · never asserted as fact' });
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const api = { KIND_LETTER, LEGEND, kindColor, layout, hit, shortAddr, pctText, fmtNum, whyLines };
|
|
159
|
+
root.DTBubbles = Object.freeze(api);
|
|
160
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* dexterm popup — bubble map page (task-1340). Same HUD chrome as the chart; the map is a canvas, the panels are text. */
|
|
2
|
+
.hud.bubbles .stage { min-height: 260px; }
|
|
3
|
+
.hud.bubbles #map { z-index: 1; cursor: crosshair; }
|
|
4
|
+
.hud.bubbles .depth { font-size: 11px; color: var(--dim); white-space: nowrap; }
|
|
5
|
+
.hud.bubbles .banner { padding: 6px 12px; font-size: 12px; color: var(--warn); border-bottom: 1px solid var(--border); background: rgba(245,255,45,.05); }
|
|
6
|
+
.hud.bubbles .banner.bad { color: var(--down); background: rgba(255,59,107,.06); }
|
|
7
|
+
.hud.bubbles .tip { position: absolute; z-index: 2; max-width: 360px; padding: 6px 9px; font-size: 11px; line-height: 1.35; color: var(--text);
|
|
8
|
+
background: rgba(3,9,12,.94); border: 1px solid var(--border); border-radius: 8px; pointer-events: none; white-space: pre-wrap; word-break: break-word; }
|
|
9
|
+
.hud.bubbles .tip .k { color: var(--muted); }
|
|
10
|
+
.hud.bubbles .insights { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 0 10px; padding: 5px 10px 3px; border-top: 1px solid var(--border); font-size: 11px; }
|
|
11
|
+
.hud.bubbles .insights .cell .l { color: var(--dim); text-transform: uppercase; letter-spacing: .04em; font-size: 10px; }
|
|
12
|
+
.hud.bubbles .insights .cell .v { color: var(--text); font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
13
|
+
.hud.bubbles .insights .cell .s { color: var(--muted); font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
14
|
+
.hud.bubbles .insights .sample { grid-column: 1 / -1; color: var(--dim); font-size: 10px; padding-top: 2px; }
|
|
15
|
+
.hud.bubbles .why { padding: 4px 10px 6px; border-top: 1px solid var(--border); font-size: 11px; line-height: 1.35; max-height: 118px; overflow: auto; }
|
|
16
|
+
.hud.bubbles .why .head { color: var(--borderFocus); font-weight: 700; }
|
|
17
|
+
.hud.bubbles .why .label { display: flex; justify-content: space-between; color: var(--text); font-weight: 700; margin-top: 3px; }
|
|
18
|
+
.hud.bubbles .why .label .conf { color: var(--dim); font-weight: 400; }
|
|
19
|
+
.hud.bubbles .why .rule { color: var(--muted); padding-left: 12px; }
|
|
20
|
+
.hud.bubbles .why .evidence { color: var(--dim); font-style: italic; padding-left: 8px; }
|
|
21
|
+
.hud.bubbles .why .foot, .hud.bubbles .why .muted { color: var(--dim); }
|
|
22
|
+
.hud.bubbles .legend span { display: inline-flex; align-items: center; gap: 5px; }
|
|
23
|
+
.hud.bubbles .legend i { display: inline-block; width: 9px; height: 9px; border-radius: 50%; }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="dexterm-ws" content="__WS_TOKEN__">
|
|
6
|
+
<meta name="dexterm-popup" content="__POPUP_ID__">
|
|
7
|
+
<meta name="dexterm-port" content="__PORT__">
|
|
8
|
+
<title>dexterm · bubble map</title>
|
|
9
|
+
<style nonce="__NONCE__">/*__CSS__*/</style>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="hud" class="hud bubbles">
|
|
13
|
+
<div id="drag" class="drag">
|
|
14
|
+
<span class="mark">✦</span>
|
|
15
|
+
<span id="coin-img-slot" class="coin-img-slot" hidden></span>
|
|
16
|
+
<span id="title" class="title">dexterm · BUBBLE MAP</span>
|
|
17
|
+
<span id="depth" class="depth"></span>
|
|
18
|
+
<span id="status" class="status">connecting…</span>
|
|
19
|
+
<button id="close" class="close" type="button" aria-label="close">✕</button>
|
|
20
|
+
</div>
|
|
21
|
+
<div id="banner" class="banner" hidden></div>
|
|
22
|
+
<div class="stage">
|
|
23
|
+
<canvas id="rain"></canvas>
|
|
24
|
+
<canvas id="map"></canvas>
|
|
25
|
+
<div id="tip" class="tip" hidden></div>
|
|
26
|
+
</div>
|
|
27
|
+
<div id="insights" class="insights"></div>
|
|
28
|
+
<div id="why" class="why"></div>
|
|
29
|
+
<div id="legend" class="legend"></div>
|
|
30
|
+
<div id="foot" class="foot"><span>hover rule</span><span>click select</span><span>m treemap</span><span>z zoom cluster</span><span>esc close</span><span id="fps" class="fps">— fps</span></div>
|
|
31
|
+
</div>
|
|
32
|
+
<script nonce="__NONCE__">/*__COMMON_JS__*/</script>
|
|
33
|
+
<script nonce="__NONCE__">/*__LIB_JS__*/</script>
|
|
34
|
+
<script nonce="__NONCE__">/*__JS__*/</script>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// dexterm popup — BUBBLE MAP HUD (task-1340, wallet intel W5). The same engine output as the terminal's bubble
|
|
2
|
+
// screen, drawn as real circles on a canvas: area ∝ share, colour = rule, dotted ring + tag per linked cluster,
|
|
3
|
+
// hover shows the rule text behind a colour, click selects (the terminal hears `holder.selected`), `m` treemap,
|
|
4
|
+
// `z` zooms the selected cluster. Inlined after common.js + bubbles-lib.js under a CSP nonce. No remote content,
|
|
5
|
+
// no innerHTML: every untrusted string (addresses, rule text, symbol) goes through textContent / fillText.
|
|
6
|
+
// Data is a BubbleVM over the WS; an `unavailable` VM draws a banner and nothing else — never a fake map.
|
|
7
|
+
(() => {
|
|
8
|
+
'use strict';
|
|
9
|
+
const DT = window.DT; DT.page = 'bubbles'; DT.rainEvery = 2;
|
|
10
|
+
const B = window.DTBubbles;
|
|
11
|
+
const $ = (id) => document.getElementById(id);
|
|
12
|
+
const titleEl = $('title'), depthEl = $('depth'), bannerEl = $('banner'), tipEl = $('tip'), insightsEl = $('insights'), whyEl = $('why'), legendEl = $('legend');
|
|
13
|
+
const cv = $('map'); const ctx = cv.getContext('2d');
|
|
14
|
+
const rain = DT.mountRain('rain');
|
|
15
|
+
let vm = null, lay = null, view = 'bubbles', focus = null, hover = -1, selected = null, W = 0, H = 0, DPR = 1, dirty = true;
|
|
16
|
+
|
|
17
|
+
function clear(el) { while (el.firstChild) el.removeChild(el.firstChild); }
|
|
18
|
+
function el(tag, cls, text) { const e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }
|
|
19
|
+
|
|
20
|
+
function resize() {
|
|
21
|
+
const r = cv.parentElement.getBoundingClientRect();
|
|
22
|
+
W = Math.max(1, Math.floor(r.width)); H = Math.max(1, Math.floor(r.height)); DPR = Math.min(2, window.devicePixelRatio || 1);
|
|
23
|
+
cv.width = W * DPR; cv.height = H * DPR; cv.style.width = W + 'px'; cv.style.height = H + 'px';
|
|
24
|
+
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
25
|
+
if (rain) rain.resize(W, H);
|
|
26
|
+
lay = null; dirty = true;
|
|
27
|
+
}
|
|
28
|
+
window.addEventListener('resize', resize);
|
|
29
|
+
// the insights / WHY panels change height with the data and the hover, which resizes the stage: re-fit the canvas
|
|
30
|
+
if (typeof ResizeObserver !== 'undefined') new ResizeObserver(() => { const r = cv.parentElement.getBoundingClientRect(); if (Math.floor(r.width) !== W || Math.floor(r.height) !== H) resize(); }).observe(cv.parentElement);
|
|
31
|
+
|
|
32
|
+
function relayout() {
|
|
33
|
+
if (!vm || vm.state !== 'ok') { lay = null; return; }
|
|
34
|
+
lay = B.layout(vm.holders, vm.clusters, W - 16, H - 16, { focus });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---- legend (glyph + word, colour-blind second encoding)
|
|
38
|
+
function renderLegend() {
|
|
39
|
+
clear(legendEl);
|
|
40
|
+
const t = DT.theme;
|
|
41
|
+
for (const [kind, word] of B.LEGEND) {
|
|
42
|
+
const s = el('span'); const dot = el('i'); dot.style.background = B.kindColor(t, kind); if (kind === 'plain') dot.style.opacity = '.6';
|
|
43
|
+
s.appendChild(dot); s.appendChild(document.createTextNode(word)); legendEl.appendChild(s);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---- insights strip (sample-captioned)
|
|
48
|
+
function renderInsights() {
|
|
49
|
+
clear(insightsEl);
|
|
50
|
+
if (!vm || vm.state !== 'ok' || !vm.insights) return;
|
|
51
|
+
const I = vm.insights;
|
|
52
|
+
const pct = (v) => (v == null ? 'n/a' : `${Number(v).toFixed(1)}%`);
|
|
53
|
+
const cells = [
|
|
54
|
+
['top10 raw', pct(I.top10Raw), 'by token acct'],
|
|
55
|
+
I.top10ExPool != null ? ['top10 ex-pool', pct(I.top10ExPool), `adj ${pct(I.top10Adjusted)} · -${I.exPoolDropped || 0} venue`] : ['top10 adj', pct(I.top10Adjusted), `excl ${I.excludedCount} labelled`],
|
|
56
|
+
['snipers', I.snipers ? String(I.snipers.count) : 'n/a', I.snipers && I.snipers.count ? `${pct(I.snipers.launchSharePct)} at launch` : 'first 3 slots'],
|
|
57
|
+
['bundled', pct(I.bundlePct), `${I.cohorts == null ? '—' : I.cohorts} cohorts`],
|
|
58
|
+
['dev', I.devPct != null ? pct(I.devPct) : I.devUpperBoundPct != null ? `≤${pct(I.devUpperBoundPct)}` : 'n/a', I.devPct == null ? 'not in sample' : 'now'],
|
|
59
|
+
['fresh', pct(I.freshPct), `of ${pct(I.freshMeasuredPct)} meas.`],
|
|
60
|
+
];
|
|
61
|
+
for (const [l, v, s] of cells) { const c = el('div', 'cell'); c.appendChild(el('div', 'l', l)); c.appendChild(el('div', 'v', v)); c.appendChild(el('div', 's', s)); insightsEl.appendChild(c); }
|
|
62
|
+
if (I.bigHolder) { const r = el('div', 'sample', `⚠ main risk: ${I.bigHolder.tag} ${B.shortAddr(I.bigHolder.owner, 4)} holds ${pct(I.bigHolder.pct)} of supply`); r.style.color = DT.theme.down; r.style.fontWeight = 'bold'; insightsEl.appendChild(r); }
|
|
63
|
+
insightsEl.appendChild(el('div', 'sample', I.sample));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- WHY THIS LABEL panel for the selected (or hovered) holder
|
|
67
|
+
function renderWhy() {
|
|
68
|
+
clear(whyEl);
|
|
69
|
+
if (!vm || vm.state !== 'ok') return;
|
|
70
|
+
const idx = hover >= 0 ? hover : selected;
|
|
71
|
+
const h = idx != null && idx >= 0 ? vm.holders[idx] : null;
|
|
72
|
+
if (!h) { whyEl.appendChild(el('div', 'muted', 'hover or click a bubble to see the rules behind its colour')); return; }
|
|
73
|
+
const cluster = h.cluster ? vm.clusters.find((c) => c.name === h.cluster) : null;
|
|
74
|
+
for (const ln of B.whyLines(h, cluster)) {
|
|
75
|
+
if (ln.kind === 'label') { const row = el('div', 'label'); const left = el('span', null, ln.text); left.style.color = B.kindColor(DT.theme, ln.label === 'LP' || ln.label === 'contract' ? 'pool' : ln.label === 'multi-token-top20' ? 'whale' : ln.label); row.appendChild(left); row.appendChild(el('span', 'conf', ln.right)); whyEl.appendChild(row); }
|
|
76
|
+
else whyEl.appendChild(el('div', ln.kind, ln.text));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function banner(text, bad) { if (!text) { bannerEl.hidden = true; return; } bannerEl.hidden = false; bannerEl.textContent = text; bannerEl.className = 'banner' + (bad ? ' bad' : ''); }
|
|
81
|
+
|
|
82
|
+
// ---- drawing
|
|
83
|
+
function rgba(hex, a) { return DT.rgba(hex, a); }
|
|
84
|
+
function draw() {
|
|
85
|
+
ctx.clearRect(0, 0, W, H);
|
|
86
|
+
if (!vm) return;
|
|
87
|
+
const t = DT.theme;
|
|
88
|
+
if (vm.state !== 'ok') { ctx.font = '13px ui-monospace, Menlo, monospace'; ctx.fillStyle = t.muted; ctx.textAlign = 'center'; ctx.fillText(vm.state === 'loading' ? 'scanning holders…' : 'bubble map unavailable', W / 2, H / 2); return; }
|
|
89
|
+
if (!lay) relayout();
|
|
90
|
+
if (!lay) return;
|
|
91
|
+
const ox = 8, oy = 8;
|
|
92
|
+
if (view === 'treemap') return drawTreemap(ox, oy, W - 16, H - 16);
|
|
93
|
+
ctx.font = '11px ui-monospace, Menlo, monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
94
|
+
// rings + tags
|
|
95
|
+
for (const cl of lay.clusters) {
|
|
96
|
+
if (!cl.multi) continue;
|
|
97
|
+
const col = B.kindColor(t, cl.kind);
|
|
98
|
+
ctx.save(); ctx.setLineDash([3, 4]); ctx.strokeStyle = rgba(col, 0.55); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(ox + cl.cx, oy + cl.cy, cl.R + 2, 0, Math.PI * 2); ctx.stroke(); ctx.restore();
|
|
99
|
+
ctx.fillStyle = col; ctx.font = 'bold 11px ui-monospace, Menlo, monospace';
|
|
100
|
+
ctx.fillText(`${B.KIND_LETTER[cl.kind] || '·'}·${cl.name} ${cl.members.length}w ${cl.pct.toFixed(1)}%`, ox + cl.cx, Math.max(8, oy + cl.cy - cl.R - 9));
|
|
101
|
+
}
|
|
102
|
+
// dotted edges from the selected holder to its mates
|
|
103
|
+
const sel = selected != null ? lay.bubbles.find((b) => b.idx === selected) : null;
|
|
104
|
+
if (sel) {
|
|
105
|
+
const cl = lay.clusters.find((c) => c.members.includes(selected));
|
|
106
|
+
if (cl && cl.multi) { ctx.save(); ctx.setLineDash([2, 4]); ctx.strokeStyle = rgba(t.accent, 0.7); for (const m of cl.members) { if (m === selected) continue; const mb = lay.bubbles.find((b) => b.idx === m); ctx.beginPath(); ctx.moveTo(ox + sel.cx, oy + sel.cy); ctx.lineTo(ox + mb.cx, oy + mb.cy); ctx.stroke(); } ctx.restore(); }
|
|
107
|
+
}
|
|
108
|
+
// discs
|
|
109
|
+
for (const b of lay.bubbles) {
|
|
110
|
+
const h = vm.holders[b.idx];
|
|
111
|
+
const isSel = b.idx === selected, isHover = b.idx === hover;
|
|
112
|
+
const col = isSel ? t.borderFocus : B.kindColor(t, h.kind);
|
|
113
|
+
const g = ctx.createRadialGradient(ox + b.cx - b.r * 0.3, oy + b.cy - b.r * 0.3, b.r * 0.1, ox + b.cx, oy + b.cy, b.r);
|
|
114
|
+
g.addColorStop(0, rgba(col, h.kind === 'plain' && !isSel ? 0.55 : 0.95)); g.addColorStop(1, rgba(col, h.kind === 'plain' && !isSel ? 0.3 : 0.7));
|
|
115
|
+
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(ox + b.cx, oy + b.cy, b.r, 0, Math.PI * 2); ctx.fill();
|
|
116
|
+
if (isHover || isSel) { ctx.strokeStyle = isSel ? t.borderFocus : t.text; ctx.lineWidth = isSel ? 2 : 1.5; ctx.stroke(); }
|
|
117
|
+
if (b.r >= 14) {
|
|
118
|
+
ctx.fillStyle = '#031008'; ctx.font = `${isSel ? 'bold ' : ''}${b.r >= 30 ? 12 : 10}px ui-monospace, Menlo, monospace`;
|
|
119
|
+
const addr = B.shortAddr(h.owner, 4), pct = B.pctText(h.pct);
|
|
120
|
+
if (b.r >= 24) { ctx.fillText(addr, ox + b.cx, oy + b.cy - 7); ctx.fillText(pct, ox + b.cx, oy + b.cy + 6); if (b.r >= 36 && h.kind !== 'plain') ctx.fillText(h.kind.toUpperCase(), ox + b.cx, oy + b.cy + 19); }
|
|
121
|
+
else ctx.fillText(pct, ox + b.cx, oy + b.cy);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (lay.overflow) { ctx.fillStyle = t.warn; ctx.font = '11px ui-monospace, Menlo, monospace'; ctx.textAlign = 'left'; ctx.fillText('window too small to pack the map: grid view', ox + 4, oy + 10); }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function drawTreemap(x, y, w, h) {
|
|
128
|
+
const t = DT.theme;
|
|
129
|
+
const items = vm.clusters.slice().sort((a, b) => b.pct - a.pct).map((cl) => ({ cl, v: Math.max(0.05, cl.pct) }));
|
|
130
|
+
const total = items.reduce((s, it) => s + it.v, 0) || 1;
|
|
131
|
+
// simple slice-and-dice rows (the terminal keeps the squarified version; this view is the share reading)
|
|
132
|
+
let cy = y, remain = h;
|
|
133
|
+
let i = 0;
|
|
134
|
+
while (i < items.length && remain > 2) {
|
|
135
|
+
let rowV = 0, j = i;
|
|
136
|
+
const target = Math.max(items[i].v, total * (remain / h) * 0.28);
|
|
137
|
+
while (j < items.length && rowV < target) rowV += items[j++].v;
|
|
138
|
+
const rh = Math.max(14, Math.round((rowV / total) * h));
|
|
139
|
+
let cx = x;
|
|
140
|
+
for (let k = i; k < j; k++) {
|
|
141
|
+
const cl = items[k].cl;
|
|
142
|
+
const rw = Math.round((items[k].v / rowV) * w);
|
|
143
|
+
const col = selected != null && cl.members.includes(vm.holders[selected] && vm.holders[selected].owner) ? t.borderFocus : B.kindColor(t, cl.kind);
|
|
144
|
+
ctx.fillStyle = rgba(col, 0.22); ctx.fillRect(cx, cy, rw - 1, rh - 1);
|
|
145
|
+
ctx.fillStyle = col; ctx.fillRect(cx, cy, 2, rh - 1);
|
|
146
|
+
if (rw > 44) { ctx.fillStyle = col; ctx.font = 'bold 11px ui-monospace, Menlo, monospace'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillText(cl.members.length > 1 ? `${B.KIND_LETTER[cl.kind] || '·'}·${cl.name}` : B.shortAddr(cl.members[0], 3), cx + 6, cy + 3); ctx.fillStyle = t.text; ctx.font = '11px ui-monospace, Menlo, monospace'; if (rh > 28) ctx.fillText(`${cl.pct.toFixed(1)}%${cl.members.length > 1 ? ` ${cl.members.length}w` : ''}`, cx + 6, cy + 16); }
|
|
147
|
+
cx += rw;
|
|
148
|
+
}
|
|
149
|
+
cy += rh; remain -= rh; i = j;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---- hover / click
|
|
154
|
+
function hitAt(e) {
|
|
155
|
+
if (!lay || view !== 'bubbles') return -1;
|
|
156
|
+
const r = cv.getBoundingClientRect();
|
|
157
|
+
return B.hit(lay, e.clientX - r.left - 8, e.clientY - r.top - 8);
|
|
158
|
+
}
|
|
159
|
+
cv.addEventListener('mousemove', (e) => {
|
|
160
|
+
const idx = hitAt(e);
|
|
161
|
+
if (idx !== hover) { hover = idx; dirty = true; renderWhy(); }
|
|
162
|
+
if (idx >= 0) {
|
|
163
|
+
const h = vm.holders[idx];
|
|
164
|
+
clear(tipEl);
|
|
165
|
+
tipEl.appendChild(el('div', 'k', `${B.shortAddr(h.owner, 6)} · ${B.pctText(h.pct)} · ${h.kind}${h.cluster ? ` · ${h.cluster}` : ''}`));
|
|
166
|
+
const first = h.labels && h.labels[0];
|
|
167
|
+
const cl0 = h.cluster ? (vm.clusters || []).find((c) => c.name === h.cluster) : null;
|
|
168
|
+
const ev0 = cl0 ? (cl0.evidence || []).find((e) => e.linked !== false && (e.a === h.owner || e.b === h.owner)) : null;
|
|
169
|
+
tipEl.appendChild(el('div', null, first ? `${first.label}: ${first.rule}` : ev0 ? `linked: ${ev0.rule}` : 'no rule matched · unlinked wallet'));
|
|
170
|
+
tipEl.hidden = false;
|
|
171
|
+
const r = cv.getBoundingClientRect();
|
|
172
|
+
const tx = Math.min(e.clientX - r.left + 14, W - 370), ty = Math.min(e.clientY - r.top + 14, H - 60);
|
|
173
|
+
tipEl.style.left = `${Math.max(4, tx)}px`; tipEl.style.top = `${Math.max(4, ty)}px`;
|
|
174
|
+
} else tipEl.hidden = true;
|
|
175
|
+
});
|
|
176
|
+
cv.addEventListener('mouseleave', () => { hover = -1; tipEl.hidden = true; dirty = true; renderWhy(); });
|
|
177
|
+
cv.addEventListener('click', (e) => {
|
|
178
|
+
const idx = hitAt(e);
|
|
179
|
+
if (idx < 0) return;
|
|
180
|
+
selected = idx; dirty = true; renderWhy();
|
|
181
|
+
const owner = vm.holders[idx] && vm.holders[idx].owner;
|
|
182
|
+
if (owner && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(owner)) DT.send({ type: 'event', name: 'holder.selected', payload: { address: owner } });
|
|
183
|
+
});
|
|
184
|
+
window.addEventListener('keydown', (e) => {
|
|
185
|
+
if (e.key === 'm') { view = view === 'treemap' ? 'bubbles' : 'treemap'; dirty = true; }
|
|
186
|
+
else if (e.key === 'z') { if (focus) focus = null; else if (selected != null && vm && vm.holders[selected] && vm.holders[selected].cluster) focus = vm.holders[selected].cluster; lay = null; dirty = true; }
|
|
187
|
+
else return;
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
function setData(a) {
|
|
192
|
+
vm = a;
|
|
193
|
+
const sym = String(vm.symbol || '?').replace(/^\$/, '');
|
|
194
|
+
titleEl.textContent = `dexterm · $${sym} · BUBBLE MAP`;
|
|
195
|
+
document.title = `dexterm · ${sym} bubbles`;
|
|
196
|
+
depthEl.textContent = vm.state === 'ok' ? `${vm.depth || 'light'} scan${vm.via ? ` · ${vm.via}` : ''}` : '';
|
|
197
|
+
if (vm.state === 'unavailable') banner(`bubble map unavailable: ${vm.reason || 'no holder source answered'}${vm.fix ? ` — ${vm.fix}` : ''}`, true);
|
|
198
|
+
else if (vm.state === 'loading') banner('scanning holders…', false);
|
|
199
|
+
else banner(vm.degraded ? `partial: ${vm.degraded}` : null, false);
|
|
200
|
+
if (vm.selected != null && vm.selected !== selected) selected = vm.selected;
|
|
201
|
+
if (focus && !(vm.clusters || []).some((c) => c.name === focus)) focus = null;
|
|
202
|
+
if (rain) rain.setSources([sym, String(vm.mint || '').slice(0, 6), ...((vm.holders || []).slice(0, 6).map((h) => String(h.owner).slice(-6)))]);
|
|
203
|
+
DT.status(vm.state === 'ok' ? `${vm.holders.length} holders` : vm.state, vm.state === 'ok' ? 'up' : vm.state === 'loading' ? 'warn' : 'down');
|
|
204
|
+
renderInsights(); renderWhy();
|
|
205
|
+
resize(); // the panels just changed height; fit the canvas to what is left before laying out
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
DT.onData(setData);
|
|
209
|
+
DT.onTheme(() => { renderLegend(); renderWhy(); dirty = true; });
|
|
210
|
+
DT.wireChrome();
|
|
211
|
+
resize(); renderLegend(); renderWhy();
|
|
212
|
+
DT.connect();
|
|
213
|
+
DT.loop(() => { if (dirty) { dirty = false; draw(); } });
|
|
214
|
+
})();
|