yaml-flow 6.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/board-live-cards-cli.js +4 -4
- package/browser/asset-integrity.json +3 -3
- package/browser/board-livecards-client.js +2 -0
- package/browser/board-livecards-client.js.map +1 -0
- package/browser/board-livecards-localstorage.js +10 -0
- package/browser/board-livecards-localstorage.js.map +1 -0
- package/browser/board-livegraph-engine.js +2 -2
- package/browser/board-livegraph-engine.js.map +1 -1
- package/browser/card-compute.js +28 -28
- package/browser/compute-jsonata.js +5 -0
- package/browser/compute-jsonata.js.map +1 -0
- package/browser/live-cards.js +264 -151
- package/card-store.js +4 -4
- package/dist/{board-live-cards-public-CltXYgaY.d.cts → board-live-cards-public-5n1-syA3.d.cts} +8 -5
- package/dist/{board-live-cards-public-f-E-FAyp.d.ts → board-live-cards-public-CK_J8uv0.d.ts} +8 -5
- package/dist/board-livegraph-runtime/index.cjs +2 -2
- package/dist/board-livegraph-runtime/index.cjs.map +1 -1
- package/dist/board-livegraph-runtime/index.d.cts +11 -9
- package/dist/board-livegraph-runtime/index.d.ts +11 -9
- package/dist/board-livegraph-runtime/index.js +2 -2
- package/dist/board-livegraph-runtime/index.js.map +1 -1
- package/dist/board-livegraph-runtime/jsonata-sync.cjs +37 -1
- package/dist/card-compute/index.cjs +4 -4
- package/dist/card-compute/index.cjs.map +1 -1
- package/dist/card-compute/index.d.cts +5 -1
- package/dist/card-compute/index.d.ts +5 -1
- package/dist/card-compute/index.js +4 -4
- package/dist/card-compute/index.js.map +1 -1
- package/dist/card-compute/jsonata-sync.cjs +37 -1
- package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs +2 -1
- package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs.map +1 -1
- package/dist/cli/browser-api/board-live-cards-browser-adapter.d.cts +27 -14
- package/dist/cli/browser-api/board-live-cards-browser-adapter.d.ts +27 -14
- package/dist/cli/browser-api/board-live-cards-browser-adapter.js +2 -1
- package/dist/cli/browser-api/board-live-cards-browser-adapter.js.map +1 -1
- package/dist/cli/browser-api/card-store-browser-api.cjs +1 -1
- package/dist/cli/browser-api/card-store-browser-api.cjs.map +1 -1
- package/dist/cli/browser-api/card-store-browser-api.js +1 -1
- package/dist/cli/browser-api/card-store-browser-api.js.map +1 -1
- package/dist/cli/browser-api/jsonata-sync.cjs +37 -1
- package/dist/cli/node/artifacts-store-cli.cjs +8 -8
- package/dist/cli/node/artifacts-store-cli.cjs.map +1 -1
- package/dist/cli/node/artifacts-store-cli.js +8 -8
- package/dist/cli/node/artifacts-store-cli.js.map +1 -1
- package/dist/cli/node/board-live-cards-cli.cjs +7 -7
- package/dist/cli/node/board-live-cards-cli.cjs.map +1 -1
- package/dist/cli/node/board-live-cards-cli.js +7 -7
- package/dist/cli/node/board-live-cards-cli.js.map +1 -1
- package/dist/cli/node/card-store-cli.cjs +5 -5
- package/dist/cli/node/card-store-cli.cjs.map +1 -1
- package/dist/cli/node/card-store-cli.js +5 -5
- package/dist/cli/node/card-store-cli.js.map +1 -1
- package/dist/cli/node/execution-adapter.cjs +3 -0
- package/dist/cli/node/execution-adapter.cjs.map +1 -0
- package/dist/cli/node/execution-adapter.d.cts +174 -0
- package/dist/cli/node/execution-adapter.d.ts +174 -0
- package/dist/cli/node/execution-adapter.js +3 -0
- package/dist/cli/node/execution-adapter.js.map +1 -0
- package/dist/cli/node/fs-board-adapter.cjs +7 -7
- package/dist/cli/node/fs-board-adapter.cjs.map +1 -1
- package/dist/cli/node/fs-board-adapter.d.cts +2 -2
- package/dist/cli/node/fs-board-adapter.d.ts +2 -2
- package/dist/cli/node/fs-board-adapter.js +7 -7
- package/dist/cli/node/fs-board-adapter.js.map +1 -1
- package/dist/cli/node/jsonata-sync.cjs +37 -1
- package/dist/cli/node/source-cli-task-executor.cjs +4 -4
- package/dist/cli/node/source-cli-task-executor.cjs.map +1 -1
- package/dist/cli/node/source-cli-task-executor.js +4 -4
- package/dist/cli/node/source-cli-task-executor.js.map +1 -1
- package/dist/continuous-event-graph/index.cjs +2 -2
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.js +2 -2
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/continuous-event-graph/jsonata-sync.cjs +37 -1
- package/dist/execution-refs.cjs +2 -1
- package/dist/execution-refs.cjs.map +1 -1
- package/dist/execution-refs.d.cts +55 -12
- package/dist/execution-refs.d.ts +55 -12
- package/dist/execution-refs.js +2 -1
- package/dist/execution-refs.js.map +1 -1
- package/dist/index.cjs +10 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +10 -10
- package/dist/index.js.map +1 -1
- package/dist/jsonata-sync.cjs +37 -1
- package/dist/server-runtime/index.cjs +9 -0
- package/dist/server-runtime/index.cjs.map +1 -0
- package/dist/server-runtime/index.d.cts +31 -0
- package/dist/server-runtime/index.d.ts +31 -0
- package/dist/server-runtime/index.js +9 -0
- package/dist/server-runtime/index.js.map +1 -0
- package/dist/server-runtime/jsonata-sync.cjs +7623 -0
- package/dist/step-machine-public/index.cjs +3 -0
- package/dist/step-machine-public/index.cjs.map +1 -0
- package/dist/step-machine-public/index.d.cts +166 -0
- package/dist/step-machine-public/index.d.ts +166 -0
- package/dist/step-machine-public/index.js +3 -0
- package/dist/step-machine-public/index.js.map +1 -0
- package/dist/step-machine-public/jsonata-sync.cjs +7623 -0
- package/dist/storage-refs.cjs +2 -2
- package/dist/storage-refs.cjs.map +1 -1
- package/dist/storage-refs.d.cts +6 -6
- package/dist/storage-refs.d.ts +6 -6
- package/dist/storage-refs.js +2 -2
- package/dist/storage-refs.js.map +1 -1
- package/dist/types-CU3DjTKL.d.cts +147 -0
- package/dist/types-HGDTWIun.d.ts +147 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-t4.js +9 -10
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-http-test.js +370 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-http-test.py +398 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-public.js +9 -10
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.js +300 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.py +617 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-sse-worker.js +48 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.py +11 -10
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +19 -4
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +4 -8
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +6 -10
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/poll-status-cli.js +8 -16
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +2 -6
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +4 -8
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +3 -7
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +4 -8
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +7 -16
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +2 -6
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/_board_pycli.py +13 -3
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/add-cards.py +2 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/init-board.py +2 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/poll-status.py +2 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +20 -24
- package/examples/cli/step-machine-cli/portfolio-tracker/run-inline-python-demo-pycli.py +0 -3
- package/examples/cli/step-machine-demo/jsonata-init-board-cli.js +8 -13
- package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +33 -9
- package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +3 -1
- package/examples/cli/step-machine-demo/step2-double-cli.js +6 -12
- package/examples/cli/step-machine-demo/two-step-math.flow.yaml +66 -4
- package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +13 -5
- package/examples/example-board/agent-instructions.md +1 -1
- package/examples/example-board/cards/card-my-identity.json +30 -6
- package/examples/example-board/cards/card-portfolio-action.json +24 -6
- package/examples/example-board/cards/card-portfolio-intelligence.json +97 -0
- package/examples/example-board/cards/card-portfolio-risks.json +24 -6
- package/examples/example-board/cards/card-rebalance-impact.json +22 -6
- package/examples/example-board/cards/card-rebalance-sim.json +66 -15
- package/examples/example-board/cards/cardT-market-prices.json +80 -0
- package/examples/example-board/cards/{card-portfolio-value.json → cardT-portfolio-value.json} +38 -10
- package/examples/example-board/cards/cardT-portfolio.json +78 -0
- package/examples/example-board/demo-server-config.json +1 -1
- package/examples/example-board/demo-server.js +383 -69
- package/examples/example-board/demo-shell-localstorage.html +774 -0
- package/examples/example-board/demo-shell-with-server.html +18 -36
- package/examples/example-board/demo-shell.html +5 -4
- package/examples/example-board/demo-task-executor.js +213 -265
- package/package.json +15 -13
- package/step-machine-cli.js +43 -310
- package/board-livecards-server-runtime.js +0 -1513
- package/browser/board-livecards-runtime-client.js +0 -263
- package/dist/pycli/quickjs-board-runtime.global.js +0 -9
- package/dist/pycli/quickjs-board-runtime.global.js.map +0 -1
- package/dist/pycli/quickjs-step-machine-runtime.global.js +0 -5
- package/dist/pycli/quickjs-step-machine-runtime.global.js.map +0 -1
- package/examples/cli/step-machine-demo/two-step-math-handlers.js +0 -32
- package/examples/cli/step-machine-demo/two-step-mixed-handlers.js +0 -24
- package/examples/example-board/cards/card-market-prices.json +0 -56
- package/examples/example-board/cards/card-portfolio.json +0 -44
- package/examples/example-board/demo-shell-browser.html +0 -675
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Example Board Demo (LocalStorage Runtime)</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
|
8
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@7.1.0/browser/compute-jsonata.js"></script>
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
12
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/leader-line/leader-line.min.js"></script>
|
|
14
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@7.1.0/browser/live-cards.js"></script>
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@7.1.0/browser/board-livecards-localstorage.js"></script>
|
|
16
|
+
</head>
|
|
17
|
+
<body class="bg-light">
|
|
18
|
+
<div class="container-fluid py-3">
|
|
19
|
+
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
|
20
|
+
<div>
|
|
21
|
+
<h1 class="h4 mb-0" id="boardTitle">Example Board (LocalStorage Runtime)</h1>
|
|
22
|
+
<div class="small text-muted" id="boardDesc"></div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="d-flex align-items-center gap-2">
|
|
25
|
+
<button class="btn btn-sm btn-outline-primary" id="modeBoard">Board</button>
|
|
26
|
+
<button class="btn btn-sm btn-outline-primary" id="modeCanvas">Canvas</button>
|
|
27
|
+
<button class="btn btn-sm btn-outline-secondary" id="autoLayout">Auto Layout</button>
|
|
28
|
+
<button class="btn btn-sm btn-outline-danger" id="resetDemo">Reset Demo</button>
|
|
29
|
+
<div class="form-check ms-2">
|
|
30
|
+
<input class="form-check-input" type="checkbox" id="devModeToggle" checked />
|
|
31
|
+
<label class="form-check-label" for="devModeToggle">Dev Mode</label>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="alert alert-info small py-2 mb-3">
|
|
37
|
+
LocalStorage runtime mode: board state persisted in browser localStorage via
|
|
38
|
+
<code>board-livecards-localstorage</code> + <code>server-runtime</code>.
|
|
39
|
+
Source fetches are done by a demo task executor in this HTML using source_def url/url-list and
|
|
40
|
+
reporting results via <code>reportSourceFetched</code>.
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div id="boardRoot">
|
|
44
|
+
<div class="d-flex align-items-center justify-content-center" style="height: 72vh;">
|
|
45
|
+
<div class="text-center">
|
|
46
|
+
<div class="spinner-border mb-3" role="status">
|
|
47
|
+
<span class="visually-hidden">Loading...</span>
|
|
48
|
+
</div>
|
|
49
|
+
<p class="text-muted">Initializing board...</p>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<script>
|
|
56
|
+
(function () {
|
|
57
|
+
'use strict';
|
|
58
|
+
|
|
59
|
+
var buildBoardState = window.BoardLiveCardsLocalStorage && window.BoardLiveCardsLocalStorage.buildBoardState;
|
|
60
|
+
var applyNotification = window.BoardLiveCardsLocalStorage && window.BoardLiveCardsLocalStorage.applyNotification;
|
|
61
|
+
var createBoardLiveCardsLocalStorage = window.BoardLiveCardsLocalStorage && window.BoardLiveCardsLocalStorage.create;
|
|
62
|
+
|
|
63
|
+
if (!buildBoardState || !applyNotification || !createBoardLiveCardsLocalStorage) {
|
|
64
|
+
document.getElementById('boardRoot').innerHTML =
|
|
65
|
+
'<div class="alert alert-danger">Required scripts not loaded. Run <code>npm run build:browser</code> first.</div>';
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
var NAMESPACE = 'demo-board';
|
|
70
|
+
var currentMode = 'canvas';
|
|
71
|
+
var board = null;
|
|
72
|
+
var stateRef = { current: null }; // read accessor for resolve(); board.setState owns the state
|
|
73
|
+
var app = null;
|
|
74
|
+
var pendingNotifications = []; // buffer for notifications arriving before board is ready
|
|
75
|
+
|
|
76
|
+
function clone(x) { return JSON.parse(JSON.stringify(x)); }
|
|
77
|
+
function nowIso() { return new Date().toISOString(); }
|
|
78
|
+
|
|
79
|
+
var INLINE_CARDS = [
|
|
80
|
+
{
|
|
81
|
+
id: 'card-portfolio',
|
|
82
|
+
meta: {
|
|
83
|
+
title: 'My Portfolio',
|
|
84
|
+
tags: ['portfolio'],
|
|
85
|
+
desc: 'Manage your holdings inline. Changes propagate downstream.',
|
|
86
|
+
},
|
|
87
|
+
provides: [{ bindTo: 'holdings', ref: 'card_data.holdings' }],
|
|
88
|
+
compute: [],
|
|
89
|
+
view: {
|
|
90
|
+
elements: [
|
|
91
|
+
{
|
|
92
|
+
kind: 'editable-table',
|
|
93
|
+
label: 'Holdings',
|
|
94
|
+
data: {
|
|
95
|
+
bind: 'card_data.holdings',
|
|
96
|
+
writeTo: 'card_data.holdings',
|
|
97
|
+
columns: ['ticker', 'quantity', 'cost_basis'],
|
|
98
|
+
schema: { properties: { quantity: { type: 'number' }, cost_basis: { type: 'number' } } },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
layout: { board: { col: 4, order: 1 }, canvas: { x: 20, y: 50, w: 340, h: 330 } },
|
|
103
|
+
features: { refresh: true, chat: true },
|
|
104
|
+
},
|
|
105
|
+
card_data: {
|
|
106
|
+
holdings: [
|
|
107
|
+
{ ticker: 'AAPL', quantity: 10, cost_basis: 150 },
|
|
108
|
+
{ ticker: 'MSFT', quantity: 5, cost_basis: 310 },
|
|
109
|
+
{ ticker: 'GOOGL', quantity: 2, cost_basis: 280 },
|
|
110
|
+
{ ticker: 'TSLA', quantity: 3, cost_basis: 200 },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'card-market-prices',
|
|
116
|
+
meta: {
|
|
117
|
+
title: 'Market Prices',
|
|
118
|
+
tags: ['prices', 'market'],
|
|
119
|
+
desc: 'Fetches prices from Yahoo Finance v8 API using source_def url-list.',
|
|
120
|
+
},
|
|
121
|
+
requires: ['holdings'],
|
|
122
|
+
source_defs: [
|
|
123
|
+
{
|
|
124
|
+
bindTo: 'quotes',
|
|
125
|
+
outputFile: 'market-prices-quotes.json',
|
|
126
|
+
projections: {
|
|
127
|
+
url_list: "requires.holdings.ticker.('https://query1.finance.yahoo.com/v8/finance/chart/' & $ & '?interval=1d&range=1d')",
|
|
128
|
+
},
|
|
129
|
+
'url-list': {
|
|
130
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; portfolio-tracker-demo/1.0)' },
|
|
131
|
+
cacheTimeout: 300,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
compute: [
|
|
136
|
+
{
|
|
137
|
+
bindTo: 'normalizedQuotes',
|
|
138
|
+
expr: '{ "quoteResponse": { "result": $map(fetched_sources.quotes, function($r) { ($m := $r.chart.result[0].meta; $prev := $m.chartPreviousClose; $chg := $m.regularMarketPrice - $prev; { "symbol": $m.symbol, "shortName": $m.shortName ? $m.shortName : $m.longName, "regularMarketPrice": $m.regularMarketPrice, "regularMarketChange": $chg, "regularMarketChangePercent": $chg / $prev * 100 }) }), "error": null } }',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
bindTo: 'prices',
|
|
142
|
+
expr: '$map(computed_values.normalizedQuotes.quoteResponse.result, function($q) { {"ticker": $q.symbol, "name": $q.shortName, "price": $round($q.regularMarketPrice, 2), "change": $round($q.regularMarketChange, 2), "chg_pct": $round($q.regularMarketChangePercent, 2)} })',
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
provides: [{ bindTo: 'quotes', ref: 'computed_values.normalizedQuotes' }],
|
|
146
|
+
view: {
|
|
147
|
+
elements: [
|
|
148
|
+
{
|
|
149
|
+
kind: 'table',
|
|
150
|
+
label: 'Prices',
|
|
151
|
+
data: { bind: 'computed_values.prices', columns: ['ticker', 'name', 'price', 'change', 'chg_pct'], sortable: true },
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
layout: { board: { col: 4, order: 2 }, canvas: { x: 400, y: 50, w: 400, h: 340 } },
|
|
155
|
+
features: { refresh: true },
|
|
156
|
+
},
|
|
157
|
+
card_data: {},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'card-portfolio-value',
|
|
161
|
+
meta: {
|
|
162
|
+
title: 'Portfolio Value',
|
|
163
|
+
tags: ['portfolio', 'value'],
|
|
164
|
+
desc: 'Calculates value and P&L from holdings + quotes.',
|
|
165
|
+
},
|
|
166
|
+
requires: ['holdings', 'quotes'],
|
|
167
|
+
provides: [{ bindTo: 'positions', ref: 'computed_values.positions' }],
|
|
168
|
+
compute: [
|
|
169
|
+
{
|
|
170
|
+
bindTo: 'positions',
|
|
171
|
+
expr: '($hMap := $merge(requires.holdings.{ticker: $}); $map(requires.quotes.quoteResponse.result, function($q) { ($h := $lookup($hMap, $q.symbol); $qty := $h.quantity; $cb := $h.cost_basis; $val := $round($q.regularMarketPrice * $qty, 2); $cost := $round($cb * $qty, 2); $chg := $round($q.regularMarketChange * $qty, 2); {"ticker": $q.symbol, "quantity": $qty, "cost_basis": $cb, "price": $round($q.regularMarketPrice, 2), "value": $val, "gain_$": $round($val - $cost, 2), "gain_%": $round(($val - $cost) / $cost * 100, 2), "chg_$": $chg, "chg_pct": $round($q.regularMarketChangePercent, 2)}) }))',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
bindTo: 'totalValue',
|
|
175
|
+
expr: '$round($sum(computed_values.positions.value), 2)',
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
bindTo: 'gainers',
|
|
179
|
+
expr: '$count($filter(computed_values.positions, function($p){ $p.chg_pct > 0 }))',
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
bindTo: 'losers',
|
|
183
|
+
expr: '$count($filter(computed_values.positions, function($p){ $p.chg_pct < 0 }))',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
bindTo: 'gainersLosers',
|
|
187
|
+
expr: "computed_values.gainers & ' up · ' & computed_values.losers & ' down'",
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
view: {
|
|
191
|
+
elements: [
|
|
192
|
+
{ kind: 'metric', label: 'Portfolio Value ($)', data: { bind: 'computed_values.totalValue' } },
|
|
193
|
+
{ kind: 'text', data: { bind: 'computed_values.gainersLosers' } },
|
|
194
|
+
{ kind: 'table', label: 'Positions', data: { bind: 'computed_values.positions', columns: ['ticker', 'value', 'gain_$', 'gain_%'], sortable: true } },
|
|
195
|
+
],
|
|
196
|
+
layout: { board: { col: 4, order: 3 }, canvas: { x: 840, y: 50, w: 420, h: 380 } },
|
|
197
|
+
features: { chat: true },
|
|
198
|
+
},
|
|
199
|
+
card_data: {},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'card-portfolio-intelligence',
|
|
203
|
+
meta: {
|
|
204
|
+
title: 'Portfolio Intelligence',
|
|
205
|
+
tags: ['portfolio', 'analysis'],
|
|
206
|
+
desc: 'Derived analysis from computed positions.',
|
|
207
|
+
},
|
|
208
|
+
requires: ['positions'],
|
|
209
|
+
source_defs: [
|
|
210
|
+
{
|
|
211
|
+
bindTo: 'analysis',
|
|
212
|
+
outputFile: 'card-concentration-analysis.json',
|
|
213
|
+
kind: 'mock-llm-random',
|
|
214
|
+
prompt: 'Analyze this portfolio JSON and provide concise mix, pnl, risks, and action insights: {{positions_json}}',
|
|
215
|
+
projections: {
|
|
216
|
+
positions_json: 'requires.positions',
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
compute: [
|
|
221
|
+
{ bindTo: 'intel', expr: 'fetched_sources.analysis.intel ? fetched_sources.analysis.intel : (fetched_sources.analysis.mix ? fetched_sources.analysis.mix : fetched_sources.analysis.response)' },
|
|
222
|
+
{ bindTo: 'risk_signal', expr: 'fetched_sources.analysis.risk_signal ? fetched_sources.analysis.risk_signal : fetched_sources.analysis.risks' },
|
|
223
|
+
{ bindTo: 'action_signal', expr: 'fetched_sources.analysis.action_signal ? fetched_sources.analysis.action_signal : (fetched_sources.analysis.action ? fetched_sources.analysis.action : fetched_sources.analysis.response)' },
|
|
224
|
+
],
|
|
225
|
+
provides: [
|
|
226
|
+
{ bindTo: 'intel', ref: 'computed_values.intel' },
|
|
227
|
+
{ bindTo: 'risk_signal', ref: 'computed_values.risk_signal' },
|
|
228
|
+
{ bindTo: 'action_signal', ref: 'computed_values.action_signal' },
|
|
229
|
+
],
|
|
230
|
+
view: {
|
|
231
|
+
elements: [
|
|
232
|
+
{ kind: 'markdown', label: 'Intel', data: { bind: 'computed_values.intel' } },
|
|
233
|
+
],
|
|
234
|
+
layout: { board: { col: 3, order: 4 }, canvas: { x: 1300, y: 50, w: 340, h: 200 } },
|
|
235
|
+
features: { refresh: true, chat: true },
|
|
236
|
+
},
|
|
237
|
+
card_data: {},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'card-portfolio-risks',
|
|
241
|
+
meta: { title: 'Risk Watch', tags: ['portfolio', 'risk'], desc: 'Risk signals from Portfolio Intelligence.' },
|
|
242
|
+
requires: ['risk_signal'],
|
|
243
|
+
view: {
|
|
244
|
+
elements: [{ kind: 'markdown', data: { bind: 'requires.risk_signal' } }],
|
|
245
|
+
layout: { board: { col: 3, order: 5 }, canvas: { x: 1300, y: 270, w: 340, h: 220 } },
|
|
246
|
+
},
|
|
247
|
+
card_data: {},
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: 'card-portfolio-action',
|
|
251
|
+
meta: { title: 'Action Signal', tags: ['portfolio', 'action'], desc: 'Action recommendation from Portfolio Intelligence.' },
|
|
252
|
+
requires: ['action_signal'],
|
|
253
|
+
view: {
|
|
254
|
+
elements: [{ kind: 'markdown', data: { bind: 'requires.action_signal' } }],
|
|
255
|
+
layout: { board: { col: 4, order: 6 }, canvas: { x: 1660, y: 50, w: 300, h: 160 } },
|
|
256
|
+
},
|
|
257
|
+
card_data: {},
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
function hasPersistedBoardState() {
|
|
262
|
+
var key = NAMESPACE + ':card-store:kv:_index';
|
|
263
|
+
try { return localStorage.getItem(key) !== null; } catch { return false; }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Task executor helpers (protocol-native) ─────────────────────────────
|
|
267
|
+
|
|
268
|
+
var TASK_CACHE_KEY = NAMESPACE + ':task-executor-cache';
|
|
269
|
+
|
|
270
|
+
function readTaskCache() {
|
|
271
|
+
try {
|
|
272
|
+
var raw = localStorage.getItem(TASK_CACHE_KEY);
|
|
273
|
+
return raw ? JSON.parse(raw) : {};
|
|
274
|
+
} catch (_) { return {}; }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function writeTaskCache(cache) {
|
|
278
|
+
try { localStorage.setItem(TASK_CACHE_KEY, JSON.stringify(cache)); } catch (_) {}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
var TASK_EXECUTOR_CAPABILITIES = {
|
|
282
|
+
version: '1.0',
|
|
283
|
+
executor: 'portfolio-tracker-browser-task-executor',
|
|
284
|
+
subcommands: ['run-source-fetch', 'describe-capabilities'],
|
|
285
|
+
sourceKinds: {
|
|
286
|
+
'mock-llm-random': {
|
|
287
|
+
description: 'Return a pre-cooked LLM-like response after a random delay based on source_def.prompt.',
|
|
288
|
+
inputSchema: {
|
|
289
|
+
kind: { type: 'string', required: true, description: 'Must be "mock-llm-random".' },
|
|
290
|
+
prompt: { type: 'string', required: true, description: 'Prompt text used to pick a pre-cooked response.' },
|
|
291
|
+
bindTo: { type: 'string', required: true, description: 'Output token for fetched source.' },
|
|
292
|
+
outputFile: { type: 'string', required: false, description: 'Optional output artifact filename.' },
|
|
293
|
+
},
|
|
294
|
+
outputShape: '{ response: string, latency_ms: number, prompt_echo: string, intel?: string, risk_signal?: string, action_signal?: string }',
|
|
295
|
+
},
|
|
296
|
+
mock: {
|
|
297
|
+
description: 'Return static in-page mock data by source_def.mock key.',
|
|
298
|
+
inputSchema: {
|
|
299
|
+
mock: { type: 'string', required: true, description: 'Key in in-page MOCK_DB.' },
|
|
300
|
+
bindTo: { type: 'string', required: true, description: 'Output token for fetched source.' },
|
|
301
|
+
outputFile: { type: 'string', required: false, description: 'Optional output artifact filename.' },
|
|
302
|
+
},
|
|
303
|
+
outputShape: 'any JSON value from MOCK_DB[mock].',
|
|
304
|
+
},
|
|
305
|
+
url: {
|
|
306
|
+
description: 'Fetch JSON from a single URL described by source_def.url.url.',
|
|
307
|
+
inputSchema: {
|
|
308
|
+
url: { type: 'object', required: true, description: 'Object containing url string and optional headers/args.' },
|
|
309
|
+
bindTo: { type: 'string', required: true, description: 'Output token for fetched source.' },
|
|
310
|
+
outputFile: { type: 'string', required: false, description: 'Optional output artifact filename.' },
|
|
311
|
+
},
|
|
312
|
+
outputShape: 'JSON object from fetched URL.',
|
|
313
|
+
},
|
|
314
|
+
'url-list': {
|
|
315
|
+
description: 'Fetch JSON from each resolved URL in source_def._projections.url_list.',
|
|
316
|
+
inputSchema: {
|
|
317
|
+
'url-list': { type: 'object', required: true, description: 'Object with optional request headers.' },
|
|
318
|
+
bindTo: { type: 'string', required: true, description: 'Output token for fetched source.' },
|
|
319
|
+
outputFile: { type: 'string', required: false, description: 'Optional output artifact filename.' },
|
|
320
|
+
_projections: { type: 'object', required: true, description: 'Must include resolved url_list array.' },
|
|
321
|
+
},
|
|
322
|
+
outputShape: 'JSON array, one element per fetched URL.',
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
var MOCK_DB = {
|
|
328
|
+
analysis: {
|
|
329
|
+
intel: '- Local demo mode: using mock analysis source.\n- Provide a custom executor for copilot/workiq in CLI hosts.',
|
|
330
|
+
risk_signal: '- TSLA: elevated volatility.\n- Portfolio concentration is moderate in tech names.',
|
|
331
|
+
action_signal: '- Hold positions; rebalance only if single-name concentration crosses 35%.',
|
|
332
|
+
// Back-compat aliases for persisted old card definitions / cached payloads.
|
|
333
|
+
mix: '- Local demo mode: using mock analysis source.\n- Provide a custom executor for copilot/workiq in CLI hosts.',
|
|
334
|
+
risks: '- TSLA: elevated volatility.\n- Portfolio concentration is moderate in tech names.',
|
|
335
|
+
action: '- Hold positions; rebalance only if single-name concentration crosses 35%.',
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
function interpolateTemplate(template, values) {
|
|
340
|
+
return String(template || '').replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, function (_m, key) {
|
|
341
|
+
var v = values && Object.prototype.hasOwnProperty.call(values, key) ? values[key] : '';
|
|
342
|
+
if (v === null || v === undefined) return '';
|
|
343
|
+
if (typeof v === 'object') {
|
|
344
|
+
try { return JSON.stringify(v); } catch (_e) { return String(v); }
|
|
345
|
+
}
|
|
346
|
+
return String(v);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function resolveUrlsForSourceDef(sourceDef) {
|
|
351
|
+
if (sourceDef.url && sourceDef.url.url) {
|
|
352
|
+
var context = Object.assign({}, sourceDef._projections || {}, (sourceDef.url.args || {}));
|
|
353
|
+
return [interpolateTemplate(sourceDef.url.url, context)];
|
|
354
|
+
}
|
|
355
|
+
if (sourceDef['url-list']) {
|
|
356
|
+
return Array.isArray(sourceDef._projections && sourceDef._projections.url_list)
|
|
357
|
+
? sourceDef._projections.url_list
|
|
358
|
+
: [];
|
|
359
|
+
}
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
var DEFAULT_QUOTES = {
|
|
364
|
+
AAPL: { symbol: 'AAPL', longName: 'Apple Inc.', regularMarketPrice: 290, regularMarketChange: 0.8, regularMarketChangePercent: 0.28 },
|
|
365
|
+
MSFT: { symbol: 'MSFT', longName: 'Microsoft Corp.', regularMarketPrice: 420, regularMarketChange: -0.6, regularMarketChangePercent: -0.14 },
|
|
366
|
+
GOOGL: { symbol: 'GOOGL', longName: 'Alphabet Inc.', regularMarketPrice: 395, regularMarketChange: 1.2, regularMarketChangePercent: 0.30 },
|
|
367
|
+
TSLA: { symbol: 'TSLA', longName: 'Tesla Inc.', regularMarketPrice: 399, regularMarketChange: -2.1, regularMarketChangePercent: -0.52 },
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
function buildDefaultYahooQuoteResponse(url) {
|
|
371
|
+
var m = String(url || '').match(/\/chart\/([^?]+)/);
|
|
372
|
+
var symbol = m ? decodeURIComponent(m[1]).toUpperCase() : 'UNKNOWN';
|
|
373
|
+
var quote = DEFAULT_QUOTES[symbol] || {
|
|
374
|
+
symbol: symbol,
|
|
375
|
+
longName: symbol,
|
|
376
|
+
regularMarketPrice: 150,
|
|
377
|
+
regularMarketChange: 0,
|
|
378
|
+
regularMarketChangePercent: 0,
|
|
379
|
+
};
|
|
380
|
+
return {
|
|
381
|
+
chart: {
|
|
382
|
+
result: [{ meta: quote }],
|
|
383
|
+
error: null,
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function buildSyntheticYahooQuoteResponse(url) {
|
|
389
|
+
var m = String(url || '').match(/\/chart\/([^?]+)/);
|
|
390
|
+
var symbol = m ? decodeURIComponent(m[1]).toUpperCase() : 'UNKNOWN';
|
|
391
|
+
var seed = symbol.split('').reduce(function (s, ch) { return s + ch.charCodeAt(0); }, 0);
|
|
392
|
+
var price = 90 + (seed % 260);
|
|
393
|
+
var change = (((seed % 21) - 10) / 10);
|
|
394
|
+
var changePct = price ? (change / price) * 100 : 0;
|
|
395
|
+
return {
|
|
396
|
+
chart: {
|
|
397
|
+
result: [{
|
|
398
|
+
meta: {
|
|
399
|
+
symbol: symbol,
|
|
400
|
+
longName: symbol,
|
|
401
|
+
regularMarketPrice: Number(price.toFixed(2)),
|
|
402
|
+
regularMarketChange: Number(change.toFixed(2)),
|
|
403
|
+
regularMarketChangePercent: Number(changePct.toFixed(2)),
|
|
404
|
+
},
|
|
405
|
+
}],
|
|
406
|
+
error: null,
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function hasValidYahooMeta(payload) {
|
|
412
|
+
return !!(
|
|
413
|
+
payload
|
|
414
|
+
&& payload.chart
|
|
415
|
+
&& Array.isArray(payload.chart.result)
|
|
416
|
+
&& payload.chart.result[0]
|
|
417
|
+
&& payload.chart.result[0].meta
|
|
418
|
+
&& typeof payload.chart.result[0].meta.symbol === 'string'
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function normalizeYahooQuotePayload(url, payload) {
|
|
423
|
+
return hasValidYahooMeta(payload) ? payload : buildDefaultYahooQuoteResponse(url);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function normalizeYahooQuoteList(urls, list) {
|
|
427
|
+
var src = Array.isArray(list) ? list : [];
|
|
428
|
+
return urls.map(function (u, idx) {
|
|
429
|
+
return normalizeYahooQuotePayload(u, src[idx]);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function pickRandomItem(arr) {
|
|
434
|
+
if (!Array.isArray(arr) || arr.length === 0) return '';
|
|
435
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function resolveMockLlmResponse(prompt) {
|
|
439
|
+
var p = String(prompt || '').toLowerCase();
|
|
440
|
+
if (p.indexOf('portfolio') >= 0 || p.indexOf('holding') >= 0 || p.indexOf('ticker') >= 0) {
|
|
441
|
+
return pickRandomItem([
|
|
442
|
+
'Portfolio summary: concentration is moderate; rebalance only if a single position exceeds 35%.',
|
|
443
|
+
'Portfolio insight: momentum is mixed today, with gains led by high-beta positions.',
|
|
444
|
+
'Portfolio note: maintain allocation discipline and review downside risk before adding exposure.',
|
|
445
|
+
]);
|
|
446
|
+
}
|
|
447
|
+
if (p.indexOf('risk') >= 0 || p.indexOf('volatility') >= 0 || p.indexOf('drawdown') >= 0) {
|
|
448
|
+
return pickRandomItem([
|
|
449
|
+
'Risk view: near-term volatility is elevated; consider tighter stop-loss thresholds.',
|
|
450
|
+
'Risk view: correlation risk is rising; diversify across non-overlapping factors.',
|
|
451
|
+
'Risk view: downside tails are widening; reduce leverage and preserve optionality.',
|
|
452
|
+
]);
|
|
453
|
+
}
|
|
454
|
+
if (p.indexOf('action') >= 0 || p.indexOf('recommend') >= 0 || p.indexOf('next step') >= 0) {
|
|
455
|
+
return pickRandomItem([
|
|
456
|
+
'Suggested action: hold current positions and reassess after the next session close.',
|
|
457
|
+
'Suggested action: trim top concentration by 5% and rotate into lower-volatility names.',
|
|
458
|
+
'Suggested action: stage entries over multiple intervals instead of a single fill.',
|
|
459
|
+
]);
|
|
460
|
+
}
|
|
461
|
+
return pickRandomItem([
|
|
462
|
+
'Mock LLM response: scenario remains neutral with balanced upside and downside signals.',
|
|
463
|
+
'Mock LLM response: wait for confirmation before changing allocation materially.',
|
|
464
|
+
'Mock LLM response: maintain risk controls and monitor regime shift indicators.',
|
|
465
|
+
]);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function buildMockLlmAnalysis(prompt) {
|
|
469
|
+
var p = String(prompt || '').toLowerCase();
|
|
470
|
+
if (p.indexOf('portfolio') >= 0 || p.indexOf('positions') >= 0 || p.indexOf('ticker') >= 0) {
|
|
471
|
+
return {
|
|
472
|
+
intel: pickRandomItem([
|
|
473
|
+
'- Largest exposure remains concentrated in mega-cap tech names.\n- Diversification is moderate across current holdings.',
|
|
474
|
+
'- Portfolio mix is growth-tilted with moderate single-name concentration.\n- P&L contribution is uneven across current positions.',
|
|
475
|
+
]),
|
|
476
|
+
risk_signal: pickRandomItem([
|
|
477
|
+
'- Primary risk is concentration and correlation during volatility spikes.\n- Monitor drawdown risk in top-weight positions.',
|
|
478
|
+
'- Risk posture is medium-high due to momentum clustering.\n- Downside sensitivity increases in gap-down sessions.',
|
|
479
|
+
]),
|
|
480
|
+
action_signal: pickRandomItem([
|
|
481
|
+
'- Hold core positions and rebalance only if top holding breaches concentration threshold.',
|
|
482
|
+
'- Trim strongest outperformer modestly and redistribute into lower-volatility exposure.',
|
|
483
|
+
]),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
var generic = resolveMockLlmResponse(prompt);
|
|
488
|
+
return {
|
|
489
|
+
intel: generic,
|
|
490
|
+
risk_signal: generic,
|
|
491
|
+
action_signal: generic,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function delayedMockLlmResult(prompt) {
|
|
496
|
+
var delayMs = 1000 + Math.floor(Math.random() * 2001);
|
|
497
|
+
await new Promise(function (resolve) { setTimeout(resolve, delayMs); });
|
|
498
|
+
var analysis = buildMockLlmAnalysis(prompt);
|
|
499
|
+
return {
|
|
500
|
+
response: analysis.intel || resolveMockLlmResponse(prompt),
|
|
501
|
+
latency_ms: delayMs,
|
|
502
|
+
prompt_echo: String(prompt || ''),
|
|
503
|
+
intel: analysis.intel,
|
|
504
|
+
risk_signal: analysis.risk_signal,
|
|
505
|
+
action_signal: analysis.action_signal,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Attempt a real network fetch for a single URL. Returns null on failure. */
|
|
510
|
+
function fetchUrlFromNetwork(url, headers) {
|
|
511
|
+
return fetch(url, { method: 'GET', headers: headers || {} })
|
|
512
|
+
.then(function (resp) { if (!resp.ok) throw new Error('HTTP ' + resp.status); return resp.json(); })
|
|
513
|
+
.catch(function () { return null; });
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
async function bootstrap() {
|
|
518
|
+
var initCards = hasPersistedBoardState() ? [] : clone(INLINE_CARDS);
|
|
519
|
+
|
|
520
|
+
// Task executor executes only declared source kinds.
|
|
521
|
+
// No shell-side cache or computed-value synthesis.
|
|
522
|
+
app = createBoardLiveCardsLocalStorage(NAMESPACE, {
|
|
523
|
+
cards: initCards,
|
|
524
|
+
taskExecutor: async function (_ref, args) {
|
|
525
|
+
if (args && (args.subcommand === 'describe-capabilities' || args.command === 'describe-capabilities')) {
|
|
526
|
+
return { dispatched: true, data: clone(TASK_EXECUTOR_CAPABILITIES) };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
var sourceDef = args && args.source_def;
|
|
530
|
+
if (!sourceDef || !sourceDef.bindTo) return { dispatched: true };
|
|
531
|
+
|
|
532
|
+
var callbackToken = args.callback && args.callback.token;
|
|
533
|
+
if (!callbackToken) return { dispatched: true };
|
|
534
|
+
|
|
535
|
+
// Extract cardId from callback token
|
|
536
|
+
var cardId = 'unknown';
|
|
537
|
+
try {
|
|
538
|
+
var decoded = JSON.parse(atob(callbackToken.replace(/-/g, '+').replace(/_/g, '/')));
|
|
539
|
+
cardId = decoded.cid || 'unknown';
|
|
540
|
+
} catch (_) {}
|
|
541
|
+
|
|
542
|
+
var bindTo = sourceDef.bindTo;
|
|
543
|
+
var urls = resolveUrlsForSourceDef(sourceDef);
|
|
544
|
+
var headers = sourceDef.url && sourceDef.url.headers
|
|
545
|
+
? sourceDef.url.headers
|
|
546
|
+
: (sourceDef['url-list'] && sourceDef['url-list'].headers ? sourceDef['url-list'].headers : {});
|
|
547
|
+
|
|
548
|
+
var resultValue = null;
|
|
549
|
+
if (sourceDef.kind === 'mock-llm-random') {
|
|
550
|
+
if (typeof sourceDef.prompt !== 'string' || sourceDef.prompt.trim() === '') {
|
|
551
|
+
app.reportSourceFetchFailure(callbackToken, 'mock-llm-random requires non-empty prompt');
|
|
552
|
+
return { dispatched: false, error: 'mock-llm-random requires non-empty prompt' };
|
|
553
|
+
}
|
|
554
|
+
var resolvedPrompt = interpolateTemplate(sourceDef.prompt, sourceDef._projections || {});
|
|
555
|
+
resultValue = await delayedMockLlmResult(resolvedPrompt);
|
|
556
|
+
} else if (sourceDef.mock) {
|
|
557
|
+
resultValue = MOCK_DB[sourceDef.mock];
|
|
558
|
+
if (resultValue === undefined) {
|
|
559
|
+
app.reportSourceFetchFailure(callbackToken, 'mock key not found: ' + sourceDef.mock);
|
|
560
|
+
return { dispatched: false, error: 'mock key not found: ' + sourceDef.mock };
|
|
561
|
+
}
|
|
562
|
+
/* Yahoo Finance url/url-list branch disabled — re-enable to fetch live quotes.
|
|
563
|
+
} else if (sourceDef.url || sourceDef['url-list']) {
|
|
564
|
+
if (!Array.isArray(urls) || urls.length === 0) {
|
|
565
|
+
app.reportSourceFetchFailure(callbackToken, 'no resolved URLs for source_def');
|
|
566
|
+
return { dispatched: false, error: 'no resolved URLs for source_def' };
|
|
567
|
+
}
|
|
568
|
+
var requestKey = JSON.stringify(urls);
|
|
569
|
+
var cache = readTaskCache();
|
|
570
|
+
if (!cache.completed) cache.completed = {};
|
|
571
|
+
if (!cache.completed[cardId]) cache.completed[cardId] = {};
|
|
572
|
+
|
|
573
|
+
var cachedEntry = cache.completed[cardId][bindTo];
|
|
574
|
+
var cachedValue = null;
|
|
575
|
+
if (cachedEntry && typeof cachedEntry === 'object' && !Array.isArray(cachedEntry)
|
|
576
|
+
&& Object.prototype.hasOwnProperty.call(cachedEntry, '_requestKey')
|
|
577
|
+
&& Object.prototype.hasOwnProperty.call(cachedEntry, 'value')) {
|
|
578
|
+
if (cachedEntry._requestKey === requestKey) cachedValue = cachedEntry.value;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (cachedValue === null) {
|
|
582
|
+
var seeded = sourceDef.url
|
|
583
|
+
? buildDefaultYahooQuoteResponse(urls[0])
|
|
584
|
+
: urls.map(function (u) { return buildDefaultYahooQuoteResponse(u); });
|
|
585
|
+
cache.completed[cardId][bindTo] = { _requestKey: requestKey, value: clone(seeded) };
|
|
586
|
+
writeTaskCache(cache);
|
|
587
|
+
cachedValue = seeded;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Ensure cached payload shape always matches quote compute expectations.
|
|
591
|
+
cachedValue = sourceDef.url
|
|
592
|
+
? normalizeYahooQuotePayload(urls[0], cachedValue)
|
|
593
|
+
: normalizeYahooQuoteList(urls, cachedValue);
|
|
594
|
+
|
|
595
|
+
resultValue = cachedValue;
|
|
596
|
+
|
|
597
|
+
// Fire-and-forget refresh: updates localStorage cache for next invocation.
|
|
598
|
+
Promise.all(urls.map(function (u) { return fetchUrlFromNetwork(u, headers); }))
|
|
599
|
+
.then(function (networkResults) {
|
|
600
|
+
var hasAny = networkResults.some(function (r) { return r !== null; });
|
|
601
|
+
if (!hasAny) return;
|
|
602
|
+
|
|
603
|
+
var freshCache = readTaskCache();
|
|
604
|
+
if (!freshCache.completed) freshCache.completed = {};
|
|
605
|
+
if (!freshCache.completed[cardId]) freshCache.completed[cardId] = {};
|
|
606
|
+
|
|
607
|
+
var currentEntry = freshCache.completed[cardId][bindTo];
|
|
608
|
+
var currentValue = (currentEntry && currentEntry.value !== undefined) ? currentEntry.value : cachedValue;
|
|
609
|
+
var nextValue;
|
|
610
|
+
|
|
611
|
+
if (sourceDef.url) {
|
|
612
|
+
nextValue = networkResults[0] !== null
|
|
613
|
+
? normalizeYahooQuotePayload(urls[0], networkResults[0])
|
|
614
|
+
: normalizeYahooQuotePayload(urls[0], currentValue);
|
|
615
|
+
} else {
|
|
616
|
+
var base = normalizeYahooQuoteList(urls, currentValue);
|
|
617
|
+
networkResults.forEach(function (res, idx) {
|
|
618
|
+
if (res !== null) base[idx] = normalizeYahooQuotePayload(urls[idx], res);
|
|
619
|
+
});
|
|
620
|
+
nextValue = base;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
freshCache.completed[cardId][bindTo] = { _requestKey: requestKey, value: clone(nextValue) };
|
|
624
|
+
writeTaskCache(freshCache);
|
|
625
|
+
})
|
|
626
|
+
.catch(function () { keep cache as-is on refresh failure });
|
|
627
|
+
*/
|
|
628
|
+
} else if (sourceDef.url || sourceDef['url-list']) {
|
|
629
|
+
// Seeded synthetic quotes; no network fetch.
|
|
630
|
+
if (!Array.isArray(urls) || urls.length === 0) {
|
|
631
|
+
app.reportSourceFetchFailure(callbackToken, 'no resolved URLs for source_def');
|
|
632
|
+
return { dispatched: false, error: 'no resolved URLs for source_def' };
|
|
633
|
+
}
|
|
634
|
+
resultValue = sourceDef.url
|
|
635
|
+
? buildDefaultYahooQuoteResponse(urls[0])
|
|
636
|
+
: urls.map(function (u) { return buildDefaultYahooQuoteResponse(u); });
|
|
637
|
+
} else {
|
|
638
|
+
app.reportSourceFetchFailure(callbackToken, 'unsupported source kind in browser executor');
|
|
639
|
+
return { dispatched: false, error: 'unsupported source kind in browser executor' };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
var blobKey = cardId + '/' + (sourceDef.outputFile || bindTo + '.json');
|
|
643
|
+
var blobRef = app.writeMemoryBlob(blobKey, JSON.stringify(resultValue));
|
|
644
|
+
app.reportSourceFetched(callbackToken, blobRef);
|
|
645
|
+
|
|
646
|
+
return { dispatched: true };
|
|
647
|
+
},
|
|
648
|
+
onWarn: function (msg) { console.warn('[localstorage-runtime]', msg); },
|
|
649
|
+
onBoardChange: function (event) {
|
|
650
|
+
if (!event || !Array.isArray(event.notifications) || event.notifications.length === 0) return;
|
|
651
|
+
if (!board) {
|
|
652
|
+
// Board not yet created — buffer for replay after initialization.
|
|
653
|
+
pendingNotifications.push.apply(pendingNotifications, event.notifications);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (board.core && board.core.devMode) {
|
|
657
|
+
console.log('[notifications]', event.notifications);
|
|
658
|
+
}
|
|
659
|
+
board.setState(function (prev) {
|
|
660
|
+
var next = applyNotification(prev, event.notifications, function () {
|
|
661
|
+
return app.getState();
|
|
662
|
+
});
|
|
663
|
+
stateRef.current = next;
|
|
664
|
+
return next;
|
|
665
|
+
});
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
document.getElementById('boardTitle').textContent = 'Portfolio Tracker (LocalStorage Runtime)';
|
|
670
|
+
document.getElementById('boardDesc').textContent =
|
|
671
|
+
'Cards are inline in HTML. Runtime state is persisted in localStorage.';
|
|
672
|
+
|
|
673
|
+
await app.bootstrap();
|
|
674
|
+
|
|
675
|
+
// Build initial state: apply any notifications buffered during bootstrap
|
|
676
|
+
// (the persisted-state snapshot from publishPersistedStateSnapshot), then
|
|
677
|
+
// fall back to a direct getState() read for fresh starts where no snapshot
|
|
678
|
+
// was published.
|
|
679
|
+
stateRef.current = pendingNotifications.length > 0
|
|
680
|
+
? applyNotification(buildBoardState(null, null), pendingNotifications, function () { return app.getState(); })
|
|
681
|
+
: buildBoardState(app.getState(), null);
|
|
682
|
+
pendingNotifications = [];
|
|
683
|
+
|
|
684
|
+
var engine = LiveCard.init({
|
|
685
|
+
resolve: function (id) { return stateRef.current && stateRef.current.modelsById[id]; },
|
|
686
|
+
chartLib: (typeof Chart !== 'undefined') ? Chart : null,
|
|
687
|
+
markdown: (typeof marked !== 'undefined') ? function (t) { return marked.parse(t); } : null,
|
|
688
|
+
sanitize: (typeof DOMPurify !== 'undefined') ? function (t) { return DOMPurify.sanitize(t); } : null,
|
|
689
|
+
onPatchState: async function (id, patch) {
|
|
690
|
+
await app.patchCard(id, patch || {});
|
|
691
|
+
},
|
|
692
|
+
onRefresh: async function (id) {
|
|
693
|
+
await app.patchCard(id, {});
|
|
694
|
+
},
|
|
695
|
+
onAction: async function (id, actionType, payload) {
|
|
696
|
+
await app.applyCardAction(id, actionType, payload || {});
|
|
697
|
+
},
|
|
698
|
+
getChatMessages: function (id) {
|
|
699
|
+
return app.readChatRecords(id).map(function (m) {
|
|
700
|
+
return {
|
|
701
|
+
role: m && typeof m.role === 'string' ? m.role : 'system',
|
|
702
|
+
text: m && typeof m.text === 'string' ? m.text : '',
|
|
703
|
+
files: Array.isArray(m && m.files) ? m.files : [],
|
|
704
|
+
};
|
|
705
|
+
});
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
document.getElementById('boardRoot').innerHTML = '';
|
|
710
|
+
|
|
711
|
+
board = LiveCard.Board(engine, document.getElementById('boardRoot'), {
|
|
712
|
+
initialState: stateRef.current,
|
|
713
|
+
getNodeIds: function (s) { return s.cardIds; },
|
|
714
|
+
selectNode: function (s, id) { return s.modelsById[id]; },
|
|
715
|
+
mode: currentMode,
|
|
716
|
+
canvas: {},
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
if (board && document.getElementById('devModeToggle').checked) {
|
|
720
|
+
board.core.setDevMode(true);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
document.getElementById('modeBoard').addEventListener('click', function () {
|
|
724
|
+
currentMode = 'board';
|
|
725
|
+
if (board) board.core.setMode('board');
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
document.getElementById('modeCanvas').addEventListener('click', function () {
|
|
729
|
+
currentMode = 'canvas';
|
|
730
|
+
if (board) board.core.setMode('canvas');
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
document.getElementById('autoLayout').addEventListener('click', function () {
|
|
734
|
+
currentMode = 'canvas';
|
|
735
|
+
if (board) {
|
|
736
|
+
board.core.setMode('canvas');
|
|
737
|
+
board.core.autoLayout();
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
document.getElementById('devModeToggle').addEventListener('change', function () {
|
|
742
|
+
if (board) board.core.setDevMode(this.checked);
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function resetDemoStorage() {
|
|
747
|
+
localStorage.clear();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function showResetOverlay() {
|
|
751
|
+
var overlay = document.createElement('div');
|
|
752
|
+
overlay.style.cssText = 'position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.55);';
|
|
753
|
+
overlay.innerHTML = '<div style="color:#fff;font-size:2rem;font-weight:600;letter-spacing:0.05em;">Resetting\u2026</div>';
|
|
754
|
+
document.body.appendChild(overlay);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
document.getElementById('resetDemo').addEventListener('click', function () {
|
|
758
|
+
showResetOverlay();
|
|
759
|
+
setTimeout(function () {
|
|
760
|
+
resetDemoStorage();
|
|
761
|
+
window.location.reload();
|
|
762
|
+
}, 400);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
bootstrap().catch(function (err) {
|
|
766
|
+
console.error(err);
|
|
767
|
+
document.getElementById('boardRoot').innerHTML =
|
|
768
|
+
'<div class="alert alert-danger">Failed to start LocalStorage runtime demo: ' +
|
|
769
|
+
String(err && err.message || err) + '</div>';
|
|
770
|
+
});
|
|
771
|
+
})();
|
|
772
|
+
</script>
|
|
773
|
+
</body>
|
|
774
|
+
</html>
|