Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
26d30cf
Run tests without bindings in Node
claude Sep 23, 2026
2f833db
Keep a selection as it was entered; keys only name thumbnails
claude Sep 23, 2026
bd56576
Render configuration thumbnails in a workflow again
claude Sep 23, 2026
a6e10ad
Lay the configuration panel out on a grid, and size its dropdowns
claude Sep 23, 2026
191dc7c
Draw shared styles from classes and theme defaults
claude Sep 23, 2026
2086282
Add component tests, and report an expression of the wrong kind
claude Sep 23, 2026
f30cbc5
Keep more tests out of the Workers pool, and reset the database in on…
claude Sep 23, 2026
131f1ae
Allow for the options an assembly insert may omit
claude Sep 23, 2026
6adf934
Read thumbnails from a workspace branched off the version
claude Sep 23, 2026
177c430
Name thumbnail branches FRCDesignApp Thumbnails (DO NOT EDIT)
claude Sep 23, 2026
3231d81
Leave build issues stored before they carried values as they are
claude Sep 23, 2026
c0dd007
Index assemblies too, and decide indexed parameters in the app
claude Sep 23, 2026
eeb6be2
Give parameters roles, and fill derivation variables with a unique value
claude Sep 23, 2026
aa60c82
Recognize only a text parameter as a derivation variable
claude Sep 23, 2026
630db86
Open Onshape links on the caller's company domain
claude Sep 23, 2026
ee92a17
Keep row icons their size beside a long title
claude Sep 23, 2026
44f946f
Stick the home page's section headers to the top
claude Sep 23, 2026
740397d
Replace apiPath with plain template strings
claude Sep 23, 2026
8db5870
Stop exporting what nothing imports
claude Sep 23, 2026
ad88f96
Stick each section header only over its own list
claude Sep 23, 2026
f7796a4
Open hover cards on a tap too, without opening the row under them
claude Sep 23, 2026
a9600fc
Reload on new Onshape versions, and re-ask access on admin team changes
claude Sep 23, 2026
07642cb
Push job, library, thumbnail and access changes to clients
claude Sep 23, 2026
23855ed
Register Onshape webhooks on the owner's behalf, without a button
claude Sep 24, 2026
c581c6d
Load one document at a time, and give each library an admin team
claude Sep 24, 2026
35b970c
Rebuild search with every load, and set the owner
claude Sep 24, 2026
b05124c
Store parameter roles, drop legacy upgrades, and tidy records and units
claude Sep 24, 2026
2ee1143
Spin only the groups loading, drop polling, and show quantities in th…
claude Sep 24, 2026
1fad2a0
Simplify thumbnail renders; prefer undefined to null
claude Sep 24, 2026
d69f63a
Tighten comment guidance and apply it; simplify AppHoverCard
claude Sep 24, 2026
de2b16c
Rename notices, share external links, tidy row parts, test change order
claude Sep 24, 2026
cf28dba
Drop TruncatedText, move the thumbnail reload item into library
claude Sep 24, 2026
19cf8ca
Move component defaults into the theme; decide redirects on the server
claude Sep 24, 2026
247fa98
Restore new versions into one thumbnail workspace per document
claude Sep 24, 2026
77f7dcb
Load webhook versions as their creator; flag loads to rerun; reload p…
claude Sep 24, 2026
668eacc
Loads find an admin session themselves; drop team webhooks; restore r…
claude Sep 24, 2026
ad1e9bc
Store a library's admin team on its row
claude Sep 24, 2026
d72a2f7
Cache workspace units in KV behind a transient webhook; declare every…
claude Sep 24, 2026
3b1f52b
Tag analytics parts with their library; plain admin team field; unsig…
claude Sep 24, 2026
3f4c666
Save and reset the favorite's configuration from the insert menu
claude Sep 24, 2026
5be7204
Simplify the insert menu's configuration plumbing
claude Sep 24, 2026
95d58bb
Hold new versions for an admin's approval
claude Sep 25, 2026
68ff163
Load held versions after two days, and badge them
claude Sep 25, 2026
f1ed595
Rename the live feature to push and tighten its types
claude Sep 25, 2026
5c3c19a
Tidy load routes and results
claude Sep 25, 2026
44f48e6
Fold this branch's migrations into one
claude Sep 25, 2026
0ee8f47
Replace a group's running load instead of queueing behind it
claude Sep 25, 2026
2d13af4
Total a library's headline tiles over the selected range
claude Sep 25, 2026
bd5782b
Delete stale thumbnails per document instead of on a cron
claude Sep 25, 2026
995e420
Keep ui-state in one Zustand store, local only, and drop synced settings
claude Sep 25, 2026
a7bde5e
Split the Onshape launch into its own store
claude Sep 26, 2026
eca01cd
Persist the stores with Zustand's own storage
claude Sep 26, 2026
d7206a5
Simplify sign-in and let tracking run itself in the background
claude Sep 26, 2026
097ff2a
Give each auth cookie its own module and a matching name
claude Sep 26, 2026
31af559
Read search terms as spans, and simplify matching and highlighting
claude Sep 26, 2026
205eaa9
Leave the search index to rebuild with its library
claude Sep 26, 2026
4f95ff7
Update package versions
AlexKempen Sep 26, 2026
0f20bfe
Rename config items
AlexKempen Sep 27, 2026
e34b328
Settings and toast polish, and make webhooks self-healing and observable
claude Sep 27, 2026
3ac09a5
Match the reset items' tests to their new labels
claude Sep 27, 2026
be0066f
Restore the lockfile as it was pushed
claude Sep 27, 2026
90298b8
Focus the document url when the add document menu opens
claude Sep 27, 2026
0ba7d44
Branch a thumbnail workspace per version, key renders by their stored…
claude Sep 27, 2026
e39a5f1
Render configured thumbnails only once Onshape has rendered that conf…
claude Sep 27, 2026
d794091
Hide the focus ring on a modal's initial focus target
claude Sep 27, 2026
502590b
Give indexing its own section with a switch per parameter, assemblies…
claude Sep 27, 2026
5bf46b7
Render configured thumbnails by predictable thumbnail id again
claude Sep 27, 2026
8b39f82
Render configured thumbnails from the thumbnail workspace only
claude Sep 27, 2026
3f4ab43
Drop the webhook telemetry columns and poll thumbnail renders every 3…
claude Sep 27, 2026
6c7bd50
Retry a load's thumbnails after a minute, then every two
claude Sep 27, 2026
3254d5a
Skip webhooks Onshape can't reach, ping new ones, and drop the cache …
claude Sep 27, 2026
27546e2
Serve the dev server through a named Cloudflare tunnel instead of mkcert
claude Sep 27, 2026
658ac18
Run the dev tunnel from a shared token
claude Sep 27, 2026
1f89da7
Serve dev at dev.frcdesign.org by default and document getting the tu…
claude Sep 27, 2026
12046b7
Drop the webhook debugging aids
claude Sep 27, 2026
0a2c1bb
Let a webhook's load in dev borrow the overridden admin's session
claude Sep 27, 2026
eb15670
Give build checks a title and, where the fix isn't plain, a description
claude Sep 27, 2026
86342d7
Keep build check descriptions short, few, and behind an info icon
claude Sep 27, 2026
099499d
Add architecture docs for the core areas, with rules and a check for …
claude Sep 27, 2026
2e25bac
Name the library in every library-gated route, and drop libraryOf
claude Sep 27, 2026
f58c713
Fill derivation variables in the menu only, and name a part studio's …
claude Sep 27, 2026
4fe6663
Drop the unused SESSION_SECRET and VERBOSE_LOGGING, and note roles ex…
claude Sep 27, 2026
cf45f1a
Document search and analytics, sign-out, library-scoped guards and de…
claude Sep 27, 2026
ea9fcfd
Serve from APP_URL: OAuth's redirect uri, webhook urls and the dev host
claude Sep 27, 2026
1aaa21b
Start configuration renders with an explicit request, and retry load …
claude Sep 28, 2026
0ed9670
Rename the admin session store to background sessions, and let editor…
claude Sep 28, 2026
e52295a
Document the platform: Onshape client, errors, retries, concurrency, …
claude Sep 28, 2026
e6ff385
Keep thumbnail workspaces any library's group still names
claude Sep 28, 2026
e5977e5
Merge cert: key configuration counts by branch
claude Sep 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
- name: Test
run: npm test

- name: Check the docs name only paths that exist
run: npm run check:docs

# A migration that only passes on an empty database will break cert.
- name: Check migrations against a populated database
run: npm run check:migrations
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ dist-ssr
# Python caches, from the scripts/ helpers
__pycache__/
*.pyc

# The shared dev tunnel's token
.tunnel-token
21 changes: 21 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,27 @@
"panel": "dedicated",
"clear": true
},
"group": {
"kind": "build"
}
},
{
"label": "Launch tunnel",
"type": "npm",
"script": "tunnel",
"problemMatcher": [],
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"clear": true
}
},
{
"label": "Launch servers",
"dependsOn": ["Launch dev", "Launch tunnel"],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
Expand Down
143 changes: 111 additions & 32 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,43 @@

## Comments

Explain _why_, not _what_: the what is in the code. Don't restate a signature
(write "returns the access level, respecting the cache", not a paragraph
re-deriving the caching), and delete comments that narrate obvious steps.
Default to none. A good name and type say what a thing is; a comment is for the
_why_ a reader cannot get from the code: a workaround, a constraint from
Onshape or Cloudflare, a choice that looks wrong but isn't.

One or two lines is the usual size. Go longer only for something genuinely
hard — a protocol Onshape does not document, a fix whose reason is not visible
from the code — and then say the hard thing plainly rather than compressing it
into dense prose. **No comment is better than a long one, and a long one is
better than a short one that is wrong.** Brevity is not worth an inaccuracy.
Keep it to one line, two at most. If it needs more, the code probably wants
restructuring or the detail belongs in the commit message. The exception is a
genuinely obscure protocol, and even then aim for a short paragraph.

A comment is a claim, and a reader will believe it without checking. So:
Write it plainly and with confidence:

- **Hedge where you are actually unsure.** "as far as I can tell" and "Onshape
does not document this" are useful; they tell the next person where to look.
Confident phrasing on a guess is worse than no comment.
- **Say what is true now.** No history ("used to", "no longer", "was moved
here"), and no alternatives you didn't take ("rather than X"). Those go in
the commit message.
- **Don't hedge.** If you are unsure, check — read the docs, run it, write a
test — then state the result. Leave uncertainty in only where it cannot be
checked, such as undocumented Onshape behavior, and say so in a few words.
- **Don't restate the code.** No narrating steps, re-deriving a signature, or
listing every caller.

Update the comment in the same change as the code it describes, and delete it
when it stops being true. A stale comment outranks the code in a reader's head,
which is what makes it worse than none.
Update or delete a comment in the same change as the code it describes: a
stale comment is worse than none.

## Absent values

Prefer `undefined` to `null` for a value that is not there: an optional field
(`name?: string`), a function that finds nothing, an unset state. Where a
boundary hands us `null` — a D1 column, KV's `get`, a DOM API, an Onshape
response — convert it where it enters (`?? undefined`) rather than carrying it
inward.

`null` stays only where it has to:

- the boundary's own shape: a Drizzle column type, a row straight from a query;
- where `undefined` cannot go: a TanStack Query result, a Workflow step's
result, a JSON field that must be sent to say "clear this";
- where it means something `undefined` cannot, next to it: `null` for "none at
all" beside `undefined` for "the default".

## Components

Expand All @@ -42,6 +60,11 @@ function useIsDashboard(): boolean {
}
```

## Accessibility

Not a goal. Don't add `aria-*` attributes, screen-reader labels or keyboard
handling for their own sake; the app runs in Onshape's mouse-driven panel.

## Layout

`src/` has two sides, `backend/` (the Worker) and `frontend/` (the SPA). There
Expand All @@ -65,26 +88,82 @@ D1 tables live in `db/schema.ts`, except a feature's own: tracking's are in
they hold no foreign key into the rest. `drizzle.config.ts` lists every schema
file, so a new one has to be added there or its tables generate no migration.

KV is for what may expire or be lost: sessions, and caches that save Onshape
calls. Every key belongs to a `kvStore` (`lib/kv-store.ts`) declared beside the
code that owns it, with its prefix, value type and lifetime; don't read or
write `KV` directly. Anything that must last goes in D1.

## Configurations

A configuration takes exactly two forms, and `features/configurations/selection.ts`
is the only place either is built:

- A **selection** (`Selection`) is what someone picked: every parameter
the insertable declares, each value canonically spelled (base units, trimmed,
lowercase booleans). `toSelection` makes one out of whatever arrived — a partial map
from a search hit, a stored favorite, a request body — and every boundary
calls it. Parameter defaults are canonical too, from `parse-configuration`, so
nothing has to canonicalize one to compare against it.
- A **`ConfigurationKey`** is that selection's identity: what it overrides,
encoded as `id=value;id=value`, with hidden parameters left out. It addresses
a render — R2 keys, thumbnail urls, stored records, Onshape itself — and
`ELEMENT_DEFAULT_KEY` (the empty string) is a selection that overrides
nothing.

Raw text lives only inside the input a user is typing into. Don't add a third
form: if something needs a different view of a selection, it wants a function in
`selection.ts`, not a new shape.
- A **selection** (`Selection`) is what someone picked: every parameter the
insertable declares, each value **as it was entered**. A quantity is the
expression that was typed — `(2 + 3) in`, not `0.127 m` — and a quantity's
default is spelled in its own unit (`1 in`). `toSelection` makes one out of
whatever arrived — a search hit's values, a stored favorite, a request body,
the url — and every boundary calls it. The selection is what is stored, what
the url carries, and what Onshape is sent (`onshapeOverrides`), so a derived
feature shows the expression that was typed.
- A **`ConfigurationKey`** is derived from a selection for one purpose: naming
its thumbnail. It holds what the selection overrides, canonically spelled
(base units, hidden parameters left out), so selections rendering the same
part share a render. `DEFAULT_CONFIGURATION_KEY` (the empty string) overrides
nothing. Never store a key in place of the selection it came from, and never
send one to Onshape as a configuration outside thumbnails.

`docs/architecture/configurations.md` covers the rest of the area.

Anything that needs values compared or counted — analytics, "is this the
default" — goes through `canonicalValue`/`canonicalValues`, never through a key.
Don't add a third form: if something needs a different view of a selection, it
wants a function in `selection.ts`, not a new shape.

# Architecture docs

`docs/architecture/` holds one document per feature area, indexed in its
`README.md`: thumbnails, configurations, loading, favorites, auth (with access
levels and environment variables), search, and analytics. Each states the area's flows, storage,
**invariants**, failure modes and decisions. Read the area's document before
changing it, and check the change against its invariants.

Update the document in the same commit when a change:

- adds, removes or reorders a step of a flow it describes;
- adds, moves or drops storage — a table or column, a `kvStore`, an R2 prefix,
a browser store — or changes a lifetime, limit, retry policy or concurrency;
- adds, renames or removes an environment variable or binding;
- changes who may do something (a guard);
- moves or renames a file a document names (`npm run check:docs` fails on these);
- breaks or replaces an invariant. That is a design change: say so in the
commit message, rewrite the invariant, and add the reason under **Decisions**.

A new feature area that stores data, calls Onshape or runs in the background
gets its own document, in the shape `docs/architecture/README.md` sets out, and
a row in its index. `docs/REFERENCE.md` stays a short tour that links to these
rather than repeating them.

Write them as comments are written: what is true now, no history, no hedging.
Name code by path and symbol in backticks; describe behavior and point at the
code rather than pasting it.

When a document and the code disagree and it isn't clear which is intended,
ask rather than quietly changing either. After reading a whole document against
the code and fixing what disagrees, bump its **Last reviewed** date.

# Tests

`npm test` runs two Vitest projects. A backend test that needs bindings (D1,
R2, KV, Workflows) is named `*.worker.test.ts` and runs in the Workers runtime
against a freshly migrated D1; that setup costs far more than most tests, so
everything else — pure backend logic and the frontend — runs in Node.

Component tests are `*.test.tsx` and run in jsdom (the `dom` project).
`renderWithProviders` in `__test_utils__/render.tsx` renders the way the app
does, with a query cache that never fetches: seed what a component reads with
`setQueryData`. Test what a person does and sees — type, click, read the
screen — rather than a component's internals.

# Running the app

Expand All @@ -102,8 +181,8 @@ VITE_ACCESS_LEVEL_OVERRIDE=admin # granted by the server, and viewed by the clie
```

Then `npm run dev` (applies local D1 migrations, then serves
http://localhost:3000). The dev server goes https only when `localhost-key.pem`
and `localhost.pem` are present, so leave them out for a headless browser.
http://localhost:3000). A headless browser uses that url directly; the tunnel in
the README is only for Onshape.

The test Worker ignores `.env` (`vitest.config.ts` turns that off), so leaving
one in place does not rewrite what the auth tests assert.
Expand Down
49 changes: 21 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ _Other browsers, such as Brave, can have default security policies that prevent
Create a new file in the root of this project named `.env` and add the following contents:

```
# Server config
VERBOSE_LOGGING=true # Set to false to reduce logging output

# Onshape API Keys (Optional)
API_ACCESS_KEY=<Your API Access Key>
API_SECRET_KEY=<Your API Secret Key>
Expand All @@ -47,8 +44,8 @@ To test Onshape app changes, you will need to create an OAuth application in the
- Name: (Arbitrary) FRC Design App Test
- Primary format: (Arbitrary) com.frc-design-app.dev
- Summary: (Arbitrary) Test for the FRC Design App.
- Redirect URLs: `https://localhost:3000/auth/callback`
- OAuth URL: `https://localhost:3000/auth/sign-in`
- Redirect URLs: `https://dev.frcdesign.org/auth/callback`
- OAuth URL: `https://dev.frcdesign.org/auth/sign-in`
- Check the permissions `can read your profile information`, `can read your documents`, `can write to your documents`, and `can delete your documents and workspaces`.

Click Create application, then copy your OAuth app's OAuth client secret (from the popup) and OAuth client identifier into your `.env` file.
Expand All @@ -62,8 +59,8 @@ Next, add the necessary Extensions to your OAuth application so you can see it i
- Location: Element right panel
- Context: Inside assembly/Inside part studio
- Action URL:
- Assembly: `https://localhost:3000/init?elementType=ASSEMBLY&documentId={$documentId}&instanceType={$workspaceOrVersion}&instanceId={$workspaceOrVersionId}&elementId={$elementId}`
- Part Studio: `https://localhost:3000/init?elementType=PARTSTUDIO&documentId={$documentId}&instanceType={$workspaceOrVersion}&instanceId={$workspaceOrVersionId}&elementId={$elementId}`
- Assembly: `https://dev.frcdesign.org/init?elementType=ASSEMBLY&documentId={$documentId}&instanceType={$workspaceOrVersion}&instanceId={$workspaceOrVersionId}&elementId={$elementId}`
- Part Studio: `https://dev.frcdesign.org/init?elementType=PARTSTUDIO&documentId={$documentId}&instanceType={$workspaceOrVersion}&instanceId={$workspaceOrVersionId}&elementId={$elementId}`
- Icon: You'll need an icon. A good choice is the one at `/public/frc-design-app-dev.svg`.
4. Open the [Onshape App Store](https://cad.onshape.com/appstore/myapps) and go to My apps. Find your App and Subscribe to it.
- If it doesn't show up, try creating a Store Entry first.
Expand All @@ -83,36 +80,32 @@ Note that Onshape has an annual limit of 2,500 API calls per Onshape account. Th

In particular, avoid loading large documents into your local environment and only force reload the database when necessary.

## HTTPS Setup

Onshape requires all apps, even temporary test apps, to use https. This creates a big headache for local development.
## Tunnel Setup

You can get around this by using [mkcert](https://github.com/FiloSottile/mkcert) to create a self signed certificate which your browser will trust.
Onshape loads the app over https and delivers webhooks from its own servers, so the dev server is served at https://dev.frcdesign.org through a shared [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/). The hostname never changes, so the urls in the Onshape OAuth app are set once.

1. Install mkcert on your local machine (not in the dev container!).
If you are on Windows, this will likely mean installing [Chocolately](https://chocolatey.org/install) and running `choco install mkcert` using a Powershell terminal you run as an Administrator.
1. Create a local Certificate Authority (CA):
1. Install [cloudflared](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/) 2025.4.0 or later.
1. Get the tunnel's token from a maintainer (it is a secret; never commit it) and save it at the root of this project, where git ignores it:

```
mkcert -install
printf '%s' '<token>' > .tunnel-token
```

1. Create a localhost certificate (localhost-key.pem and localhost.pem) and copy them into the root of this project:
`npm run tunnel` runs the tunnel next to `npm run dev`; the `Launch servers` VSCode task starts both. Open the app at https://dev.frcdesign.org.

```
cd ~ # Switch to your user directory
cd Documents # Switch to the Documents folder - you can also use any other folder you recognize, like Downloads
mkcert localhost # Create a certificate which allows localhost to run
```
Only one person can use the tunnel at a time: Cloudflare spreads requests across everyone running it, so stop yours when you're done.

### Creating the tunnel (maintainers)

Done once, by someone with access to the frcdesign.org Cloudflare account:

1. You can then open your Documents folder in File Explorer and copy and paste `localhost-key.pem` and `localhost.pem` into the root of this project.
1. In the Cloudflare dashboard, open Zero Trust > Networks > Tunnels and create a Cloudflared tunnel named `frc-design-app-dev`.
1. Give it the public hostname `dev.frcdesign.org` with the service `http://localhost:3000`.
1. Copy the token from the install command the dashboard shows (the long string after `--token`). To see it again later, open the tunnel's configuration, or run `cloudflared tunnel token frc-design-app-dev` after `cloudflared tunnel login`.

If you use a chromium-based browser like Google Chrome, MKCert should install the certificate automatically.
If it doesn't, you'll need to add the Certificate Authority manually. In Firefox, the procedure is:
Refreshing the token in the dashboard revokes the old one, for when it leaks or someone leaves.

1. In PowerShell, run `mkcert -CAROOT` and note down the path.
1. Open Firefox and go to `Settings > Certificates > View Certificates... > Authorities > Import...`
1. Navigate to the `CAROOT` path and select `rootCA.pem`.
To serve your own tunnel at another hostname, set `APP_URL=https://<hostname>` in `.env` (it overrides the dev value in `wrangler.jsonc`), and use that hostname in your Onshape OAuth app.

## VSCode Setup

Expand All @@ -126,7 +119,7 @@ npm i

## Development Servers

You should now be able to run the `Launch dev` VSCode task to launch Vite.
You should now be able to run the `Launch servers` VSCode task to launch Vite and the tunnel.
You should then be able to launch the FRC Design App from the right panel of any Onshape Part Studio or Assembly and see the FRC Design App UI appear.

To see documents, add one or more documents and push a new app version to rebuild the search database.
Expand Down
Loading
Loading