Skip to content

refactor: split oversized modules into focused ones - #45

Open
AlexKempen wants to merge 14 commits into
certfrom
claude/codebase-restructure-features-snex55
Open

refactor: split oversized modules into focused ones#45
AlexKempen wants to merge 14 commits into
certfrom
claude/codebase-restructure-features-snex55

Conversation

@AlexKempen

Copy link
Copy Markdown
Collaborator

Breaks up shared/types.ts, shared/api-models.ts, backend/app.ts,
backend/auth.ts and frontend/queries.ts along their seams so the
following move to a feature layout is a pure relocation.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H

claude added 14 commits August 20, 2026 18:21
Breaks up shared/types.ts, shared/api-models.ts, backend/app.ts,
backend/auth.ts and frontend/queries.ts along their seams so the
following move to a feature layout is a pure relocation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Replaces src/shared with ownership-based placement: the backend owns
the contract (DTOs, domain enums, configuration models) and the
frontend imports it, so there is no third top-level bucket.

backend/  app.ts assembles Hono; db/ holds the client and schema; lib/
holds the Onshape client and request plumbing; features/{auth,users,
library,configurations,thumbnails,build-checker,favorites,search} each
own their routes, storage and models.

frontend/ features/<feature>/{queries.ts,components/} with cross-cutting
helpers in lib/ and shared UI in components/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
The frontend reaches the backend's DTOs and domain models through
@backend/* instead of counting ../ levels; imports within a side stay
relative.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Every module that turns an Onshape response into what we store now lives
in features/load: the parse-* modules (document contents, configurations,
configuration records, vendors, fasten info), the per-group and
per-insertable loaders, and the Workflows and job tracker that drive them.

Two boundaries were untangled to make that a clean feature:

- ThumbnailWorkflow moved to features/thumbnails, and the render/upload
  helpers moved out of its routes into store.ts. load and thumbnails
  previously imported each other in a cycle; the dependency is now
  one-way from thumbnails to load.
- parse-fasten mixed parsing with insert-time query building.
  getFastenQuery moved to features/library/insertables/fasten-query.ts,
  leaving parse-fasten to parsing alone.

configurations keeps the domain the frontend shares (models,
canonicalization, combinations, input parser); its Onshape parsers moved
to load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
auth/ had three files that all answered 'is this caller allowed' without a
line between them. Split by role instead:

- session.ts    the session cookie and its KV records
- onshape-oauth.ts  the handshake only
- caller.ts     resolving who is calling, with the KV memoization, plus
                productionCaller (was services.ts)
- guards.ts     both gates, which were one-per-file in sign-in.ts and
                access-control.ts
- routes.ts     the OAuth redirects and /access-data

/access-data reports access level and sign-in state, so it moves from
users to auth. What is left of users is the caller's stored preferences,
so it becomes settings/.

app.ts was a composition root plus the /init handler plus error handling.
/init is now features/entry (its test came along, as routes.test.ts
rather than app.test.ts), the error handler is lib/errors.ts, and the
caller binding is lib/context.ts, leaving app.ts to mount and nothing else.

AppServices is renamed Caller: it is the request's caller, not a service
registry. not-signed-in.test.ts is split into the modules it covers,
guards.test.ts and routes.test.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Matches the feature that serves it and what it actually updates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
configurations.id is a 1:1 FK to insertables.id, so LibraryOut's
configurationId was always just the insertable's own id: a boolean
wearing an id costume. Every consumer either truthiness-tested it or
passed it straight back as the id.

InsertableOut now carries isConfigurable, and callers fetch by
insertable id. The route param follows suit
(/api/configuration/:insertableId), and the configuration query keys
gain insertableConfigurationQueryMatchKey for the prefix match
insert-menu was spelling inline.

Adds tests pinning the behavior this rests on: the library response
marks configurability but carries no parameters or records, which stay
in D1 until an insertable actually needs them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
An element's part number, name and material were kept as the first entry
of configurations.records — the probe of its default configuration. Every
probed insertable therefore carried a configurations row, including ones
with no parameters to configure, and "is it configurable?" had to test the
parameter count rather than the row's existence.

That part data is not a configuration of the element, it is the element,
so it moves to insertables.part_data. configurations is left holding only
configuration data: a row exists exactly when there are parameters, and
isConfigurable is now just whether the row is there.

ConfigurationRecord becomes PartData plus the configuration that produced
it, and toRecords() recomposes the full list — the element's own data as
the record an unset configuration falls back to, then one per indexed
configuration — for search and the configuration endpoint. Both paths are
now covered by tests that fail without it.

The migration backfills part_data from records[0] (always the default
probe; see toResult), strips it from records, and drops the rows left
with no parameters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
The library reloads from Onshape, so the column starts null and
repopulates on the next load rather than being carried over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
…dions

Icons: all 34 Tabler icons map to a Phosphor equivalent, each name
checked against the installed package rather than guessed. A filled
heart becomes weight="fill" and Tabler's title prop becomes Phosphor's
alt, which renders the same <title> element.

notifications.tsx imported ReactNode from @tabler/icons-react, which
does not export it; it now comes from react.

Home page: the library and search sections were two near-identical
Accordion.Item blocks, with their open state read from a hand-built
array and written back through a branch on whether a search was active.
Both are now one list of sections, each carrying where its own open
state lives, so the value and onChange plumbing stops branching.

The accordion divider was styled onto the panel content, so a collapsed
section had no line under it. It moves to the control, which is rendered
either way; content keeps the closing line when open. Verified in the
running app.

Also deletes the beta-complete page, which nothing linked to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
useAccessData falls back to a signed-out placeholder while access-data is
in flight, so the navbar rendered the sign-in button on every load and
then removed it once the response said the caller was already signed in.
ResolvedAccessData now reports isLoaded, and the button waits for it.
Anything gated on signedIn being true was already safe — it renders
nothing until the data arrives; only the signed-out branch could flash.

The toast's action button sat in a wrap="nowrap" row with no flex-shrink
of its own, so a long message shrank it and clipped the label — 31px of
the 47px "Sign in" needed. It no longer shrinks, and the message wraps
instead.

Both verified in the running app: the button never appears across a
delayed access-data response, and the label now measures 47/47.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Mantine's Notifications containerWidth defaults to 440px and never
grows, so a message with an action button wrapped to two lines at every
window size - measured identically at 800px and 1400px wide. Setting it
to max-content sizes the toast to its content and lets Mantine clamp it
to the viewport when there is genuinely no room.

Measured across widths: 360 -> 328px/3 lines, 420 -> 388px/2 lines,
900 and 1400 -> 514px/1 line, none overflowing the viewport.

flexShrink on the action button stays: it is what keeps the label whole
in the narrow case where the row still has to wrap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UDJxumrXdB2NqtfyfsH2H
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants