driggsby 0.2.1 → 0.2.3

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.
@@ -36,6 +36,7 @@ export async function runDeploy(options, environment = defaultCredentialEnvironm
36
36
  const startedAt = io.now();
37
37
  const { outcome, createdApp } = await deployProjectFiles({ baseUrl: session.baseUrl, token: session.token }, projectDirectory, config.slug, collected, {
38
38
  live: !options.preview,
39
+ ...(config.background !== null ? { background: config.background } : {}),
39
40
  // The assigned slug is server text, so it prints quoted and
40
41
  // length-bounded like every other string this CLI did not author.
41
42
  // A validated slug tops out at 63 characters, so the line never
@@ -52,6 +52,7 @@ export async function runDev(options = {}, environment = defaultCredentialEnviro
52
52
  try {
53
53
  servers = await startDevServers({
54
54
  slug: config.slug,
55
+ background: config.background,
55
56
  serveDirectory: config.serveDirectory,
56
57
  sdkBundle,
57
58
  runToolCall: (tool, argumentsObject) => broker.runToolCall(tool, argumentsObject),
@@ -117,7 +117,7 @@ async function handleHostRequest(options, hostPort, appOrigin, sseClients, reque
117
117
  }
118
118
  const path = requestPath(request);
119
119
  if (request.method === "GET" && path === "/") {
120
- sendHostPage(response, hostPageHtml(options.slug, appOrigin), appOrigin);
120
+ sendHostPage(response, hostPageHtml(options.slug, appOrigin, options.background), appOrigin);
121
121
  return;
122
122
  }
123
123
  if (request.method === "GET" && path === "/host.js") {
@@ -8,12 +8,14 @@
8
8
  // The page's script is served as its own file so the page can carry a
9
9
  // Content-Security-Policy with no inline scripts.
10
10
  //
11
- // Both interpolations below are HTML-safe by construction, not by escaping:
11
+ // All interpolations below are HTML-safe by construction, not by escaping:
12
12
  // `slug` has passed readProjectConfig's strict slug rules (lowercase
13
- // alphanumerics and dashes only) before runDev ever calls this, and
13
+ // alphanumerics and dashes only) before runDev ever calls this,
14
14
  // `appOrigin` is built by startDevServers from a literal plus a bound port
15
- // number. Anything looser than those two sources must not be passed here.
16
- export function hostPageHtml(slug, appOrigin) {
15
+ // number, and `background` has passed readProjectConfig's strict hex-color
16
+ // rule (or is null). Anything looser than those sources must not be
17
+ // passed here.
18
+ export function hostPageHtml(slug, appOrigin, background) {
17
19
  return `<!doctype html>
18
20
  <html lang="en" data-app-origin="${appOrigin}">
19
21
  <head>
@@ -45,6 +47,9 @@ export function hostPageHtml(slug, appOrigin) {
45
47
  flex: 1;
46
48
  width: 100%;
47
49
  border: none;
50
+ /* The app's declared background from driggsby.json, painted while
51
+ the app loads — the same surface the production host shows. */
52
+ background: ${background ?? "#ffffff"};
48
53
  }
49
54
  </style>
50
55
  </head>
@@ -66,6 +71,47 @@ export const HOST_PAGE_JS = [
66
71
  "const APP_ORIGIN = document.documentElement.dataset.appOrigin;",
67
72
  'const frame = document.getElementById("app");',
68
73
  "",
74
+ "// The app's in-page location (a URL fragment), mirrored into this",
75
+ "// page's own URL and handed back in the hello — the same route sync",
76
+ "// the production host does, same strict shape on both sides.",
77
+ "const APP_ROUTE_PATTERN = /^#[A-Za-z0-9\\/\\-._]{1,256}$/;",
78
+ "",
79
+ "function sanitizeAppRoute(value) {",
80
+ ' return typeof value === "string" && APP_ROUTE_PATTERN.test(value) ? value : "";',
81
+ "}",
82
+ "",
83
+ "let appRoute = sanitizeAppRoute(window.location.hash);",
84
+ "",
85
+ "// URL writes are bounded (leading + one trailing per window) because",
86
+ "// the frame controls their rate, and each write is guarded: Safari",
87
+ "// hard-errors on rapid replaceState bursts.",
88
+ "const ROUTE_WRITE_WINDOW_MS = 1000;",
89
+ "let routeWriteTimer = null;",
90
+ "",
91
+ "function writeRouteToUrl() {",
92
+ " if (window.location.hash === appRoute) {",
93
+ " return;",
94
+ " }",
95
+ ' const base = window.location.href.split("#")[0];',
96
+ " try {",
97
+ ' history.replaceState(history.state, "", base + appRoute);',
98
+ " } catch (error) {",
99
+ " // A refused write costs only URL freshness; the next one retries.",
100
+ " }",
101
+ "}",
102
+ "",
103
+ "function applyRoute(hash) {",
104
+ " appRoute = hash;",
105
+ " if (routeWriteTimer !== null) {",
106
+ " return;",
107
+ " }",
108
+ " writeRouteToUrl();",
109
+ " routeWriteTimer = setTimeout(() => {",
110
+ " routeWriteTimer = null;",
111
+ " writeRouteToUrl();",
112
+ " }, ROUTE_WRITE_WINDOW_MS);",
113
+ "}",
114
+ "",
69
115
  "function postToApp(message) {",
70
116
  " if (frame.contentWindow) {",
71
117
  " frame.contentWindow.postMessage(message, APP_ORIGIN);",
@@ -114,7 +160,25 @@ export const HOST_PAGE_JS = [
114
160
  " }",
115
161
  " const data = event.data;",
116
162
  ' if (data.type === "ready") {',
117
- ' postToApp({ protocol: PROTOCOL, type: "hello" });',
163
+ " const hello = { protocol: PROTOCOL, type: \"hello\" };",
164
+ ' if (appRoute !== "") {',
165
+ " hello.route = appRoute;",
166
+ " }",
167
+ " postToApp(hello);",
168
+ " return;",
169
+ " }",
170
+ ' if (data.type === "route") {',
171
+ ' // A bare "#" is the explicit cleared sentinel: the app is back at',
172
+ " // its no-hash location, so the page URL drops its fragment.",
173
+ ' if (data.hash === "#") {',
174
+ ' applyRoute("");',
175
+ " return;",
176
+ " }",
177
+ " const hash = sanitizeAppRoute(data.hash);",
178
+ ' if (hash === "") {',
179
+ " return;",
180
+ " }",
181
+ " applyRoute(hash);",
118
182
  " return;",
119
183
  " }",
120
184
  ' if (data.type !== "call") {',
@@ -4,7 +4,10 @@
4
4
  // (`driggsby dev` locally, or the Driggsby console after a deploy). The
5
5
  // sample values below are invented for the template and match the real
6
6
  // tool result shapes only in structure, so the scaffold's render functions
7
- // work unchanged when live data replaces them.
7
+ // work unchanged when live data replaces them. The HTML ships a skeleton
8
+ // of the final layout so the first paint — in every context, before any
9
+ // script runs — is muted bars that the first render replaces, fading in
10
+ // at the exact same size so nothing shifts.
8
11
  export function indexHtml(slug) {
9
12
  return `<!doctype html>
10
13
  <html lang="en">
@@ -21,12 +24,56 @@ export function indexHtml(slug) {
21
24
  <p class="sample-note" id="sample-note" hidden>Sample data — your real
22
25
  accounts appear when this app runs inside Driggsby.</p>
23
26
  </header>
27
+ <!-- The bars below are skeleton placeholders. app.js replaces them
28
+ with data on its first render; edit the render functions there,
29
+ and keep the four stat bars in step with what renderOverview
30
+ draws. (The account rows are just a plausible list length —
31
+ the real count comes from the data.) -->
24
32
  <section aria-label="Overview">
25
- <div class="stat-grid" id="overview"></div>
33
+ <div class="stat-grid" id="overview" aria-busy="true">
34
+ <div class="stat">
35
+ <div class="skeleton skeleton-label"></div>
36
+ <div class="skeleton skeleton-value"></div>
37
+ </div>
38
+ <div class="stat">
39
+ <div class="skeleton skeleton-label"></div>
40
+ <div class="skeleton skeleton-value"></div>
41
+ </div>
42
+ <div class="stat">
43
+ <div class="skeleton skeleton-label"></div>
44
+ <div class="skeleton skeleton-value"></div>
45
+ </div>
46
+ <div class="stat">
47
+ <div class="skeleton skeleton-label"></div>
48
+ <div class="skeleton skeleton-value"></div>
49
+ </div>
50
+ </div>
26
51
  </section>
27
52
  <section aria-label="Accounts">
28
53
  <h2>Accounts</h2>
29
- <div id="accounts"></div>
54
+ <div id="accounts" aria-busy="true">
55
+ <div class="account-row">
56
+ <div class="account-names">
57
+ <div class="skeleton skeleton-name"></div>
58
+ <div class="skeleton skeleton-institution"></div>
59
+ </div>
60
+ <div class="skeleton skeleton-balance"></div>
61
+ </div>
62
+ <div class="account-row">
63
+ <div class="account-names">
64
+ <div class="skeleton skeleton-name"></div>
65
+ <div class="skeleton skeleton-institution"></div>
66
+ </div>
67
+ <div class="skeleton skeleton-balance"></div>
68
+ </div>
69
+ <div class="account-row">
70
+ <div class="account-names">
71
+ <div class="skeleton skeleton-name"></div>
72
+ <div class="skeleton skeleton-institution"></div>
73
+ </div>
74
+ <div class="skeleton skeleton-balance"></div>
75
+ </div>
76
+ </div>
30
77
  </section>
31
78
  </main>
32
79
  <script type="module" src="/-/driggsby-sdk.js"></script>
@@ -44,6 +91,27 @@ export const APP_JS = `// Your app's code. Edit anything — driggsby dev reload
44
91
  // Each watch is a live subscription: the callback runs with a fresh result
45
92
  // whenever your Driggsby data changes. The sample data below matches the
46
93
  // real result shapes, so the render functions work unchanged either way.
94
+ //
95
+ // These are the tools an app can watch — read-only, nothing else responds:
96
+ //
97
+ // get_overview money rollups + every linked account
98
+ // get_history balances, holdings, or debts over time
99
+ // (history_type is required: "account_balances",
100
+ // "investment_holdings", or "liabilities")
101
+ // list_accounts current balances per linked account
102
+ // list_assets_and_liabilities the full balance sheet, incl. manual items
103
+ // list_findings issues Driggsby has flagged
104
+ // list_investment_holdings current investment positions
105
+ // list_outstanding_debts linked debts with balances and rates
106
+ // list_recurring_transactions subscriptions, bills, and paychecks
107
+ // query_cash_sql SQL over cash transactions ({ sql: "..." })
108
+ // query_investment_sql SQL over investment activity ({ sql: "..." })
109
+ // search_cash_transactions bank and card transaction search
110
+ // search_investment_activity trades, dividends, and transfers
111
+ //
112
+ // Params mirror Driggsby's public MCP tools of the same names, with one
113
+ // simplification: those tools require a "reason" string, but a watch can
114
+ // leave it out — Driggsby fills one in naming the dashboard.
47
115
 
48
116
  // ---------------------------------------------------------------------------
49
117
  // Sample data. Every name and number here is invented. It paints only
@@ -104,6 +172,19 @@ function element(tag, className, text) {
104
172
  return node;
105
173
  }
106
174
 
175
+ // The page ships with a skeleton of this exact layout in its HTML — muted
176
+ // bars where the numbers will be — so the very first paint is already the
177
+ // final shape. The first render into a container replaces its skeleton and
178
+ // fades in, so data arriving reads as the page settling, never as content
179
+ // popping. classList.add is idempotent and re-adding a class never restarts
180
+ // its animation, so later live updates repaint in place without re-fading.
181
+ // aria-busy ships in the HTML so assistive tech hears "loading" until the
182
+ // first real content lands.
183
+ function revealOnce(container) {
184
+ container.classList.add("fade-in");
185
+ container.removeAttribute("aria-busy");
186
+ }
187
+
107
188
  function renderOverview(result) {
108
189
  const rollups = (result && result.summary_rollups) || {};
109
190
  const container = document.getElementById("overview");
@@ -122,6 +203,7 @@ function renderOverview(result) {
122
203
  );
123
204
  container.append(stat);
124
205
  }
206
+ revealOnce(container);
125
207
  }
126
208
 
127
209
  function renderAccounts(result) {
@@ -143,32 +225,32 @@ function renderAccounts(result) {
143
225
  row.append(names, element("div", "account-balance", formatMoney(account.current_balance)));
144
226
  container.append(row);
145
227
  }
228
+ revealOnce(container);
146
229
  }
147
230
 
148
231
  // ---------------------------------------------------------------------------
149
232
  // Live data. window.driggsby exists when the Driggsby SDK loaded — inside
150
233
  // Driggsby, or under driggsby dev. Whether anything embeds this page is
151
234
  // knowable synchronously: opened directly in a tab, no host will ever
152
- // answer, so the sample data paints right away and the note explaining it
153
- // is revealed (it ships hidden in the HTML, so an embedded page never
154
- // shows it, not even for the moment before this script runs). Embedded,
155
- // sample numbers never paint at all — a muted loading line holds the
156
- // accounts area until real data arrives.
235
+ // answer, so the sample data replaces the skeleton right away and the note
236
+ // explaining it is revealed (it ships hidden in the HTML, so an embedded
237
+ // page never shows it, not even for the moment before this script runs).
238
+ // Embedded, sample numbers never paint at all — the shipped skeleton holds
239
+ // the layout until the first real results replace it and fade in.
157
240
  // ---------------------------------------------------------------------------
158
241
 
159
242
  const standalone = window.parent === window;
160
243
  const note = document.getElementById("sample-note");
161
244
 
162
245
  if (standalone) {
246
+ // The note and the sample values paint together (this whole branch is
247
+ // one synchronous task). The reveal is kept first so an edit that ever
248
+ // makes rendering async can't paint values before their label.
249
+ if (note) note.hidden = false;
163
250
  renderOverview(SAMPLE_OVERVIEW);
164
251
  renderAccounts(SAMPLE_ACCOUNTS);
165
- if (note) note.hidden = false;
166
- } else {
167
- if (note) note.remove();
168
- const accounts = document.getElementById("accounts");
169
- if (accounts) {
170
- accounts.replaceChildren(element("div", "loading-note", "Loading your accounts…"));
171
- }
252
+ } else if (note) {
253
+ note.remove();
172
254
  }
173
255
 
174
256
  if (window.driggsby) {
@@ -235,10 +317,64 @@ h2 {
235
317
  display: none;
236
318
  }
237
319
 
238
- .loading-note {
239
- margin-top: 8px;
240
- color: var(--text-muted);
241
- font-size: 13px;
320
+ /* The skeleton ships in the HTML so the first paint is the final layout as
321
+ muted bars; the first render replaces it and fades in (see app.js). The
322
+ bar margins make each skeleton block exactly the height of the text it
323
+ stands in for, so nothing shifts when data lands. */
324
+ .skeleton {
325
+ border-radius: 4px;
326
+ background: var(--hairline);
327
+ animation: skeleton-pulse 1.6s ease-in-out infinite;
328
+ }
329
+
330
+ .skeleton-label {
331
+ width: 64px;
332
+ height: 12px;
333
+ margin: 3.75px 0 0;
334
+ }
335
+
336
+ .skeleton-value {
337
+ width: 96px;
338
+ height: 20px;
339
+ margin: 8.75px 0 5px;
340
+ }
341
+
342
+ .skeleton-name {
343
+ width: 150px;
344
+ height: 14px;
345
+ margin: 4px 0 8px;
346
+ }
347
+
348
+ .skeleton-institution {
349
+ width: 110px;
350
+ height: 12px;
351
+ margin: 4px 0;
352
+ }
353
+
354
+ .skeleton-balance {
355
+ width: 72px;
356
+ height: 14px;
357
+ }
358
+
359
+ @keyframes skeleton-pulse {
360
+ 0%, 100% { opacity: 1; }
361
+ 50% { opacity: 0.55; }
362
+ }
363
+
364
+ .fade-in {
365
+ animation: fade-in 200ms ease-out;
366
+ }
367
+
368
+ @keyframes fade-in {
369
+ from { opacity: 0; }
370
+ to { opacity: 1; }
371
+ }
372
+
373
+ @media (prefers-reduced-motion: reduce) {
374
+ .skeleton,
375
+ .fade-in {
376
+ animation: none;
377
+ }
242
378
  }
243
379
 
244
380
  .stat-grid {
@@ -281,6 +417,10 @@ h2 {
281
417
  font-variant-numeric: tabular-nums;
282
418
  }
283
419
  `;
420
+ // "background" is the template's own page background (styles.css
421
+ // --background): Driggsby paints it while the app loads, so the loading
422
+ // surface matches the app from the first frame. An agent that re-themes
423
+ // the app should keep this equal to the page's real background color.
284
424
  export function driggsbyJsonText(slug) {
285
- return `{\n "slug": ${JSON.stringify(slug)},\n "serve": "."\n}\n`;
425
+ return `{\n "slug": ${JSON.stringify(slug)},\n "serve": ".",\n "background": "#ffffff"\n}\n`;
286
426
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driggsby",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Set up and build on Driggsby, the MCP server for personal finance.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  "node": ">=18"
29
29
  },
30
30
  "dependencies": {
31
- "@driggsby/deploy": "0.2.1",
32
- "@driggsby/sdk": "0.2.1"
31
+ "@driggsby/deploy": "0.2.3",
32
+ "@driggsby/sdk": "0.2.3"
33
33
  }
34
34
  }