Skip to content

Commit 27afe8f

Browse files
committed
feat(dev,html-app): introduce a Forecaster HTML app
1 parent 17d12b8 commit 27afe8f

18 files changed

Lines changed: 4171 additions & 5 deletions

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ COURIER_HTTP_REQUEST_CONFIG_AUTH_CONFIG_VALUE="Bearer <generated-kratos-jwt>"
7878
# SECUTILS_HTML_APP_RESPONDER_ID_PDF_EXTRACTOR_MD=
7979
# SECUTILS_HTML_APP_RESPONDER_ID_ECHO=
8080
# SECUTILS_HTML_APP_RESPONDER_ID_ECHO_MD=
81+
# SECUTILS_HTML_APP_RESPONDER_ID_FORECAST=
82+
# SECUTILS_HTML_APP_RESPONDER_ID_FORECAST_MD=
8183
# SECUTILS_HTML_APP_RESPONDER_ID_LLMS_TXT=
8284
# SECUTILS_HTML_APP_RESPONDER_ID_ROBOTS_TXT=
8385
# SECUTILS_HTML_APP_RESPONDER_ID_SITEMAP_XML=

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ A handful of focused, no-signup, browser-only tools is hosted at [tools.secutils
4242
| [Markdown to HTML](https://tools.secutils.dev/md-to-html) | Self-contained HTML and PDF export from Markdown | [`/md-to-html.md`](https://tools.secutils.dev/md-to-html.md) |
4343
| [PDF Extractor](https://tools.secutils.dev/pdf) | Extract spatial text and structured JSON from PDFs, in-browser | [`/pdf.md`](https://tools.secutils.dev/pdf.md) |
4444
| [HTTP Echo / Mock Response](https://tools.secutils.dev/echo) | Build a customizable HTTP response, served as a shareable URL | [`/echo.md`](https://tools.secutils.dev/echo.md) |
45+
| [Forecast](https://tools.secutils.dev/forecast) | Fit trendlines, forecast future values, spot anomalies | [`/forecast.md`](https://tools.secutils.dev/forecast.md) |
4546

4647
The aggregate index of all skills (including specialized ones not listed above) is published at [tools.secutils.dev/llms.txt](https://tools.secutils.dev/llms.txt) following the [llmstxt.org](https://llmstxt.org/) convention. Source HTML for every tool lives in [`dev/tools/`](dev/tools/) (see [`dev/tools/AGENTS.md`](dev/tools/AGENTS.md) for the deploy/SEO recipe).
4748

149 KB
Loading
133 KB
Loading

dev/tools/forecast.html

Lines changed: 2199 additions & 0 deletions
Large diffs are not rendered by default.

dev/tools/forecast.skill.md

Lines changed: 252 additions & 0 deletions
Large diffs are not rendered by default.

dev/tools/index.html

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>Free Developer and Security Tools | Secutils.dev</title>
7-
<meta name="description" content="Free, no-signup single-page tools for developers and security engineers. Decode JWTs, SAML, X.509 chains, mock HTTP responses, convert Markdown, extract PDF text. Browser-only.">
7+
<meta name="description" content="Free, no-signup single-page tools for developers and security engineers. Decode JWTs, SAML, X.509 chains, mock HTTP responses, convert Markdown, extract PDF text, fit trendlines and forecasts. Browser-only.">
88
<meta name="robots" content="index, follow, max-image-preview:large">
99
<link rel="canonical" href="https://{{TOOLS_HOST}}/">
1010
<meta name="su-tool-path" content="/">
@@ -14,7 +14,7 @@
1414
<meta property="og:type" content="website">
1515
<meta property="og:site_name" content="Secutils.dev">
1616
<meta property="og:title" content="Free Developer &amp; Security Tools &mdash; Secutils.dev">
17-
<meta property="og:description" content="Free, no-signup single-page tools for developers and security engineers. Decode JWTs, SAML, X.509 chains, mock HTTP responses, convert Markdown, extract PDF text.">
17+
<meta property="og:description" content="Free, no-signup single-page tools for developers and security engineers. Decode JWTs, SAML, X.509 chains, mock HTTP responses, convert Markdown, extract PDF text, fit trendlines and forecasts.">
1818
<meta property="og:url" content="https://{{TOOLS_HOST}}/">
1919
<meta property="og:image" content="https://secutils.dev/docs/img/og/og-index.png">
2020
<meta property="og:image:width" content="1200">
@@ -25,7 +25,7 @@
2525
<meta name="twitter:title" content="Free Developer &amp; Security Tools &mdash; Secutils.dev">
2626
<meta name="twitter:description" content="Free, no-signup single-page tools for developers and security engineers.">
2727
<meta name="twitter:image" content="https://secutils.dev/docs/img/og/og-index.png">
28-
<script type="application/ld+json">{"@context":"https://schema.org","@type":"ItemList","name":"Secutils.dev Free Tools","url":"https://{{TOOLS_HOST}}/","itemListElement":[{"@type":"ListItem","position":1,"url":"https://{{TOOLS_HOST}}/jwt","name":"JWT Debugger"},{"@type":"ListItem","position":2,"url":"https://{{TOOLS_HOST}}/saml","name":"SAML Decoder"},{"@type":"ListItem","position":3,"url":"https://{{TOOLS_HOST}}/pem","name":"PEM Certificate Decoder"},{"@type":"ListItem","position":4,"url":"https://{{TOOLS_HOST}}/md-to-html","name":"Markdown to HTML"},{"@type":"ListItem","position":5,"url":"https://{{TOOLS_HOST}}/pdf","name":"PDF Extractor"},{"@type":"ListItem","position":6,"url":"https://{{TOOLS_HOST}}/echo","name":"HTTP Echo / Mock Response"}]}</script>
28+
<script type="application/ld+json">{"@context":"https://schema.org","@type":"ItemList","name":"Secutils.dev Free Tools","url":"https://{{TOOLS_HOST}}/","itemListElement":[{"@type":"ListItem","position":1,"url":"https://{{TOOLS_HOST}}/jwt","name":"JWT Debugger"},{"@type":"ListItem","position":2,"url":"https://{{TOOLS_HOST}}/saml","name":"SAML Decoder"},{"@type":"ListItem","position":3,"url":"https://{{TOOLS_HOST}}/pem","name":"PEM Certificate Decoder"},{"@type":"ListItem","position":4,"url":"https://{{TOOLS_HOST}}/md-to-html","name":"Markdown to HTML"},{"@type":"ListItem","position":5,"url":"https://{{TOOLS_HOST}}/pdf","name":"PDF Extractor"},{"@type":"ListItem","position":6,"url":"https://{{TOOLS_HOST}}/echo","name":"HTTP Echo / Mock Response"},{"@type":"ListItem","position":7,"url":"https://{{TOOLS_HOST}}/forecast","name":"Forecast"}]}</script>
2929
<link rel="preconnect" href="https://fonts.googleapis.com">
3030
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
3131
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=Roboto+Mono:wght@400..700&display=swap" rel="stylesheet">
@@ -170,6 +170,10 @@ <h1 class="page-title">Developer Tools</h1>
170170
<div class="tool-name">Echo <span class="tool-path">/echo</span> <span class="arrow">&rarr;</span></div>
171171
<div class="tool-desc">Build a fully customizable HTTP response (status, headers, body) and serve it as a shareable URL.</div>
172172
</a>
173+
<a class="tool-card" href="/forecast">
174+
<div class="tool-name">Forecast <span class="tool-path">/forecast</span> <span class="arrow">&rarr;</span></div>
175+
<div class="tool-desc">Fit trendlines, forecast future values, smooth noisy data, and spot anomalies in any numeric series. WASM-powered, single-page, shareable.</div>
176+
</a>
173177
</div>
174178
</main>
175179

dev/tools/js/micro-ml/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
dist/

dev/tools/js/micro-ml/README.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# `@secutils-dev/micro-ml-browser`
2+
3+
Browser-compatible Vite build of [`micro-ml`](https://github.com/AdamPerlinski/micro-ml).
4+
Produces a single self-contained ESM module at `dist/micro-ml.js` that is
5+
inlined into `dev/tools/forecast.html` at deploy time by
6+
`dev/tools/deploy.ts`'s `data-su-bundle="micro-ml"` mechanism.
7+
8+
Not published. Internal to this repo. See
9+
[`dev/tools/AGENTS.md`](../../AGENTS.md) -> "Embedded JS bundles" for the
10+
inliner contract.
11+
12+
## Build
13+
14+
```bash
15+
npm ci
16+
npm run build
17+
# => dist/micro-ml.js
18+
```
19+
20+
`make tools-bundles` from the repo root builds this (and any future
21+
bundles) in one go. `make deploy-tools` builds it on demand if
22+
`dist/micro-ml.js` is missing or older than the sources here.
23+
24+
## Pinned upstream versions
25+
26+
| dep | version | notes |
27+
|-------------|------------|---------------------------------------------------------|
28+
| `micro-ml` | `1.0.0` | exact pin -- we patch its WASM-loader path via the plugin in `vite.config.ts` |
29+
| `vite` | `^7.1.13` | bundler |
30+
| (vendored) `micro_ml_core_bg.wasm` | (~145 KB) | shipped inside `micro-ml` itself |
31+
32+
The bundle is shipped to the responder body in the `gzip-base64` encoding
33+
variant of `data-su-bundle` (~250 KB gzipped, ~340 KB base64'd). The
34+
forecast HTML loader reverses both steps via `DecompressionStream('gzip')`
35+
on first use.
36+
37+
## How the WASM gets inlined
38+
39+
Upstream `micro-ml@1.0.0`'s browser entry lazy-loads its Rust core via:
40+
41+
```js
42+
const core = await import('./micro_ml_core-CYEMXCKP.js');
43+
await core.default(); // fetches micro_ml_core_bg.wasm via new URL(..., import.meta.url)
44+
```
45+
46+
The `data-su-bundle` contract requires a **single self-contained** file --
47+
the responder body cannot ship a sibling `.wasm`. So `vite.config.ts`'s
48+
`microMlInlineWasmPlugin`:
49+
50+
1. Reads `node_modules/micro-ml/dist/micro_ml_core_bg.wasm` at build time.
51+
2. Exposes the bytes via the virtual `virtual:micro-ml-wasm-bytes`
52+
specifier (default export is a base64-decoded `Uint8Array`).
53+
3. Rewrites upstream `dist/index.js` to call
54+
`core.initSync({ module: <bytes> })` directly, bypassing the
55+
`default()` fetch path. The Node branch is left untouched; it's
56+
harmless dead code in the browser bundle.
57+
58+
If a future `micro-ml` release reshapes the minified source, the
59+
`ELSE_BRANCH_RE` regex stops matching and the build **throws** at the
60+
`transform` stage. Re-inspect `node_modules/micro-ml/dist/index.js` and
61+
update the regex.
62+
63+
## Re-syncing against a newer `micro-ml`
64+
65+
1. Bump the pin in `package.json` (exact version, not a range).
66+
2. `npm install`.
67+
3. `npm run build`. If it throws "could not find the browser branch of
68+
micro-ml's lazy WASM initialiser", open
69+
`node_modules/micro-ml/dist/index.js` and grep for `else await
70+
...default()` -- update `ELSE_BRANCH_RE` to match. The right-hand
71+
replacement (`initSync({ module })`) does not change because
72+
wasm-pack's `initSync({ module })` API is stable.
73+
4. Verify the bundle still inlines the WASM:
74+
`grep -c '"data:application/wasm' dist/micro-ml.js` should print `0`
75+
(we don't go through a data: URL; we feed bytes straight to
76+
`initSync`), and `grep -c 'fetch' dist/micro-ml.js` should be 0
77+
(no runtime fetch of the WASM).
78+
5. Smoke-test by loading the forecast tool and clicking a sample
79+
dataset: the first chart paint should happen within ~50 ms after the
80+
bundle is decoded.
81+
82+
## Runtime caveats
83+
84+
1. **First-use cost.** The bundle decodes a base64'd WASM blob and calls
85+
`WebAssembly.Module()` synchronously on first ML invocation. On a
86+
modern laptop this is ~10-30 ms. The forecast tool only loads the
87+
bundle on the first user action, so a search-result visitor never pays
88+
it.
89+
2. **Worker version is not bundled.** Upstream ships a `micro-ml/worker`
90+
subpath that wraps the API in a Web Worker. The forecast tool runs on
91+
the main thread (sub-millisecond on typical 200-point series), so we
92+
don't bundle the worker variant.
93+
3. **The Node branch is dead code in the browser bundle.** It's preserved
94+
so that running this bundle under Node would still work (defensive,
95+
not load-bearing). esbuild's minifier does not fully drop it because
96+
the `typeof globalThis.process` guard is opaque at compile time.

0 commit comments

Comments
 (0)