Clean URLs and waveform rendering fixes

Switch from query-param routing (/notebook/?id=X) to Astro dynamic
routes (/notebook/rc-lowpass-filter). Add @astrojs/node adapter with
output: 'server' for on-demand route handling.

Fix formatEng/formatAxisValue crash on null values passed by uPlot
axis tick formatters. Add CORS origin for port 4322.
This commit is contained in:
Ryan Malloy 2026-02-13 02:16:11 -07:00
parent 8abd7719bf
commit 72eb073787
10 changed files with 213 additions and 59 deletions

View File

@ -26,8 +26,10 @@ def create_app() -> FastAPI:
CORSMiddleware,
allow_origins=[
"http://localhost:4321",
"http://localhost:4322",
"http://localhost:3000",
"http://127.0.0.1:4321",
"http://127.0.0.1:4322",
"http://127.0.0.1:3000",
],
allow_credentials=True,

View File

@ -1,8 +1,11 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import node from '@astrojs/node';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
integrations: [react()],
telemetry: false,
devToolbar: { enabled: false },

View File

@ -9,6 +9,7 @@
"version": "2026.02.13",
"dependencies": {
"@astrojs/check": "^0.9.6",
"@astrojs/node": "^9.5.3",
"@astrojs/react": "^4.0.0",
"@codemirror/autocomplete": "^6.18.0",
"@codemirror/commands": "^6.7.0",
@ -173,6 +174,20 @@
"vfile": "^6.0.3"
}
},
"node_modules/@astrojs/node": {
"version": "9.5.3",
"resolved": "https://registry.npmjs.org/@astrojs/node/-/node-9.5.3.tgz",
"integrity": "sha512-72jrSn0XtrD7COJVO6TxJmyU1yXdYK7MDdN/+fhqhf4YOhxuIPHclkXrJs8FbLCMx5ur56d/1ijX4XBeneqyXQ==",
"license": "MIT",
"dependencies": {
"@astrojs/internal-helpers": "0.7.5",
"send": "^1.2.1",
"server-destroy": "^1.0.1"
},
"peerDependencies": {
"astro": "^5.14.3"
}
},
"node_modules/@astrojs/prism": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.3.0.tgz",
@ -4513,6 +4528,15 @@
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@ -4660,6 +4684,12 @@
"node": ">=4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.286",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
@ -4688,6 +4718,15 @@
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/enhanced-resolve": {
"version": "5.19.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
@ -4770,6 +4809,12 @@
"node": ">=6"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
@ -4791,6 +4836,15 @@
"@types/estree": "^1.0.0"
}
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
@ -4872,6 +4926,15 @@
"node": ">=20"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -5145,6 +5208,26 @@
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
"license": "BSD-2-Clause"
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/import-meta-resolve": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
@ -5155,6 +5238,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/iron-webcrypto": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
@ -6414,6 +6503,31 @@
],
"license": "MIT"
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@ -6531,6 +6645,18 @@
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT"
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/oniguruma-parser": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
@ -6738,6 +6864,15 @@
"integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==",
"license": "MIT"
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
@ -7100,6 +7235,44 @@
"semver": "bin/semver.js"
}
},
"node_modules/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"fresh": "^2.0.0",
"http-errors": "^2.0.1",
"mime-types": "^3.0.2",
"ms": "^2.1.3",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/server-destroy": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
"integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==",
"license": "ISC"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@ -7211,6 +7384,15 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
@ -7350,6 +7532,15 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"@astrojs/check": "^0.9.6",
"@astrojs/node": "^9.5.3",
"@astrojs/react": "^4.0.0",
"@codemirror/autocomplete": "^6.18.0",
"@codemirror/commands": "^6.7.0",

View File

@ -121,7 +121,7 @@ export default function NotebookList() {
{notebooks.map((nb) => (
<a
key={nb.id}
href={`/notebook/?id=${encodeURIComponent(nb.id)}`}
href={`/notebook/${encodeURIComponent(nb.id)}`}
className="group block rounded-lg border border-slate-700 bg-slate-800/50 p-5 hover:border-blue-500/50 hover:bg-slate-800 transition-all"
>
<div className="flex items-start justify-between mb-3">

View File

@ -25,7 +25,7 @@ export default function NewNotebookRedirect() {
((data.metadata as Record<string, unknown>)?.id as string);
if (id) {
window.location.href = `/notebook/?id=${encodeURIComponent(id)}`;
window.location.href = `/notebook/${encodeURIComponent(id)}`;
} else {
// Fallback: go back to the list where the new notebook should appear
window.location.href = '/';

View File

@ -1,39 +0,0 @@
import { useMemo } from 'react';
import NotebookEditor from './NotebookEditor';
/**
* Wrapper that extracts the notebook ID from the URL query string
* and passes it to the main editor component.
*
* URL format: /notebook/?id=<notebook-id>
*/
export default function NotebookEditorPage() {
const notebookId = useMemo(() => {
if (typeof window === 'undefined') return null;
const params = new URLSearchParams(window.location.search);
return params.get('id');
}, []);
if (!notebookId) {
return (
<div className="flex items-center justify-center min-h-screen px-4">
<div className="max-w-md w-full text-center">
<h2 className="text-lg font-semibold text-slate-300 mb-2">
No notebook selected
</h2>
<p className="text-sm text-slate-500 mb-4">
Open a notebook from the list to start editing.
</p>
<a
href="/"
className="inline-block px-4 py-2 text-sm bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-colors"
>
Browse notebooks
</a>
</div>
</div>
);
}
return <NotebookEditor notebookId={notebookId} />;
}

View File

@ -17,7 +17,8 @@ const SI_PREFIXES: [number, string][] = [
[1e-15, 'f'],
];
export function formatEng(value: number, unit?: string): string {
export function formatEng(value: number | null | undefined, unit?: string): string {
if (value == null) return '--';
if (value === 0) return `0${unit ? ' ' + unit : ''}`;
if (!isFinite(value)) return value > 0 ? '+Inf' : '-Inf';
@ -62,9 +63,10 @@ export function formatCurrent(amps: number): string {
* Format a value for an axis label based on signal type.
*/
export function formatAxisValue(
value: number,
value: number | null | undefined,
axisType: string,
): string {
if (value == null) return '--';
switch (axisType) {
case 'frequency':
return formatFreq(value);

View File

@ -0,0 +1,10 @@
---
import NotebookLayout from '../../layouts/NotebookLayout.astro';
import NotebookEditor from '../../components/notebook/NotebookEditor';
const { id } = Astro.params;
---
<NotebookLayout title="SpiceBook">
<NotebookEditor notebookId={id!} client:load />
</NotebookLayout>

View File

@ -1,16 +0,0 @@
---
/**
* Notebook editor page.
*
* The notebook ID is passed via the ?id= query parameter.
* Example: /notebook/?id=my-circuit
*
* The React island reads the ID from the URL and loads the notebook.
*/
import NotebookLayout from '../../layouts/NotebookLayout.astro';
import NotebookEditorPage from '../../components/notebook/NotebookEditorPage';
---
<NotebookLayout title="SpiceBook">
<NotebookEditorPage client:load />
</NotebookLayout>