From 72eb07378772ffc1fc5eecb180b2a2cdff6a9382 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Fri, 13 Feb 2026 02:16:11 -0700 Subject: [PATCH] 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. --- backend/src/spicebook/main.py | 2 + frontend/astro.config.mjs | 3 + frontend/package-lock.json | 191 ++++++++++++++++++ frontend/package.json | 1 + frontend/src/components/NotebookList.tsx | 2 +- .../notebook/NewNotebookRedirect.tsx | 2 +- .../notebook/NotebookEditorPage.tsx | 39 ---- frontend/src/lib/waveform-utils.ts | 6 +- frontend/src/pages/notebook/[id].astro | 10 + frontend/src/pages/notebook/index.astro | 16 -- 10 files changed, 213 insertions(+), 59 deletions(-) delete mode 100644 frontend/src/components/notebook/NotebookEditorPage.tsx create mode 100644 frontend/src/pages/notebook/[id].astro delete mode 100644 frontend/src/pages/notebook/index.astro diff --git a/backend/src/spicebook/main.py b/backend/src/spicebook/main.py index 03b5508..9bad435 100644 --- a/backend/src/spicebook/main.py +++ b/backend/src/spicebook/main.py @@ -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, diff --git a/frontend/astro.config.mjs b/frontend/astro.config.mjs index 2ad0fce..047e606 100644 --- a/frontend/astro.config.mjs +++ b/frontend/astro.config.mjs @@ -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 }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7ed3d43..10afec6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index e8f0791..126ac0c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/components/NotebookList.tsx b/frontend/src/components/NotebookList.tsx index c63fd0e..5997fc7 100644 --- a/frontend/src/components/NotebookList.tsx +++ b/frontend/src/components/NotebookList.tsx @@ -121,7 +121,7 @@ export default function NotebookList() { {notebooks.map((nb) => (
diff --git a/frontend/src/components/notebook/NewNotebookRedirect.tsx b/frontend/src/components/notebook/NewNotebookRedirect.tsx index 182614e..8904996 100644 --- a/frontend/src/components/notebook/NewNotebookRedirect.tsx +++ b/frontend/src/components/notebook/NewNotebookRedirect.tsx @@ -25,7 +25,7 @@ export default function NewNotebookRedirect() { ((data.metadata as Record)?.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 = '/'; diff --git a/frontend/src/components/notebook/NotebookEditorPage.tsx b/frontend/src/components/notebook/NotebookEditorPage.tsx deleted file mode 100644 index fdea725..0000000 --- a/frontend/src/components/notebook/NotebookEditorPage.tsx +++ /dev/null @@ -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= - */ -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 ( -
-
-

- No notebook selected -

-

- Open a notebook from the list to start editing. -

-
- Browse notebooks - -
-
- ); - } - - return ; -} diff --git a/frontend/src/lib/waveform-utils.ts b/frontend/src/lib/waveform-utils.ts index 8aecbe2..25a24d6 100644 --- a/frontend/src/lib/waveform-utils.ts +++ b/frontend/src/lib/waveform-utils.ts @@ -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); diff --git a/frontend/src/pages/notebook/[id].astro b/frontend/src/pages/notebook/[id].astro new file mode 100644 index 0000000..ec6d025 --- /dev/null +++ b/frontend/src/pages/notebook/[id].astro @@ -0,0 +1,10 @@ +--- +import NotebookLayout from '../../layouts/NotebookLayout.astro'; +import NotebookEditor from '../../components/notebook/NotebookEditor'; + +const { id } = Astro.params; +--- + + + + diff --git a/frontend/src/pages/notebook/index.astro b/frontend/src/pages/notebook/index.astro deleted file mode 100644 index 733efa9..0000000 --- a/frontend/src/pages/notebook/index.astro +++ /dev/null @@ -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'; ---- - - - -