Skip to content

v0.13.0 → v0.15.0: licensing, accounts, the navigation shell, and the engineering registry - #19

Merged
Coldbari merged 8 commits into
mainfrom
release/v0.13.0-licensing
Sep 1, 2026
Merged

v0.13.0 → v0.15.0: licensing, accounts, the navigation shell, and the engineering registry#19
Coldbari merged 8 commits into
mainfrom
release/v0.13.0-licensing

Conversation

@Coldbari

@Coldbari Coldbari commented Sep 1, 2026

Copy link
Copy Markdown
Owner

main has been trailing production by five releases. This brings it up to
v0.15.0, which is what is live at
pid-studio-praharsh.web.app today.

The branch name says v0.13.0-licensing; it grew past that and now carries
three releases.

What's in it

v0.13.0 — PolyForm licensing, accounts, cloud drawings (978d84b)

AGPL-3.0-only → PolyForm Noncommercial 1.0.0: source-available, free for
personal, academic, nonprofit and government use, paid for commercial use. Not
retroactive — v0.12.1 and earlier stay AGPL for everyone who received them.
Adds NOTICE, SECURITY.md and SPDX headers across the tree.

Plus the account layer: email/password and Google sign-in, per-account cloud
drawings in Firestore with debounced autosave, and a marketing homepage at /
with the editor at /app.

v0.14.0 — the navigation shell (efaf865)

A workspace rail (Draw · Data · Checks · HMI) with real routes and lazy chunks,
a Ctrl+K command palette that absorbs Ctrl+F, on-screen instrument index and
line list, a full-screen findings report, and a "Where used" inspector tab.

Two measured fixes: cloud saves are ~50% smaller (half the 900 kB Firestore
budget was JSON indentation — 328 B/item pretty vs 165 B/item compact), and
validation runs once per edit instead of three times (three components each held
their own useMemo(doc)).

v0.15.0 — the engineering registry (0aae247)

Engineering data gets a home of its own, keyed by tag rather than node id —
so a record survives deleting and redrawing a symbol, and follows a rename.
Four invariants (move / copy / refuse-to-merge / never-delete-on-delete), each
covered by a test.

⚠️ Document format: schemaVersion 4 → 5

Documents saved by v0.15.0 cannot be opened by v0.14.0 or earlier. Opening
older documents is unaffected — v1 through v4 all migrate in, node.datasheet
is still read as a fallback for one release, and a datasheet on an untagged
symbol is parked rather than dropped.

tests/model/examples.test.ts runs all five bundled examples (which span
schemaVersion 1–4 and are the Templates menu) through the migration and a round
trip. Every future schema bump has to keep it passing.

Fork safety

The Firebase web config is read from the environment with no hardcoded
fallback
. A hardcoded default is inherited by every fork, so a fork's users
would sign into this project — real accounts and drawings in someone else's
Firestore, against their quota. Without .env.local the app runs fully local
with the account layer switched off; .env.example documents standing up your
own project, and .firebaserc is untracked so firebase deploy can't target
this project by accident.

Verification

Run locally against this branch, and re-run under CI conditions (no
.env.local, fresh dev server) to confirm this PR goes green:

  • npm run build — clean
  • npm test714 passing
  • npx playwright test57 passing, 3 skipped (the two sign-in-gate specs
    skip when no backend is configured, which is the CI case)

Note on history

The three commits are split by logical unit so the log reads properly, but the
working tree only ever held the final state — the SPDX headers alone touch ~137
files, so the changes are interleaved at file level. Only 0aae247 (HEAD) is
verified green
; the two earlier commits are a reconstruction and may not build
in isolation. That affects git bisect across this range and nothing else.
Merge with a merge commit or rebase — not squash, which would also flatten the
legitimate v0.11.x and v0.12.0 history this branch carries.

🤖 Generated with Claude Code

Coldbari and others added 8 commits August 27, 2026 18:54
…bar, real currency conversion

Prices: DEFAULT_PRICES grows from 121 keys to 186 exact entries + 12 category
fallbacks, so every one of the 194 library symbols has its own researched price
instead of a flat category default. Researched Aug 2026 against distributor and
manufacturer listings, published cost correlations (Towler & Sinnott / Matches,
CEPCI-escalated) and tender awards.

The flat fallbacks were badly wrong in both directions: solids-handling symbols
sat at $250 (a ball mill is ~$500k, a crystallizer ~$700k), while fittings were
over-priced by the same number (a 2in reducer is ~$18). Vessels were low by
3-11x, safety/relief by 2.4-4x. A few were high: instr.analyzer 15000 -> 4000,
valve.solenoid 700 -> 340.

Each entry now also carries low/high, the basis it was priced at, and ev
('sourced' vs 'est') so the dialog can show what a number actually means.

UI: the live budget chip moves from the bottom status bar into the toolbar and
absorbs the separate plain Budget button — the running cost is something you
steer by while drawing, not a status readout. The dialog is rebuilt: summary
figures with a budget progress bar, per-row pricing basis, est badges on rows
resting on a correlation rather than a published price, outlined overrides, a
new 1.5x install factor, and real CSS classes in place of inline styles.

Currency: the selector only swapped the symbol before, printing a rupee sign in
front of a USD number. New model/currency.ts converts for real across 12
currencies at rates frozen 27 Aug 2026. Prices stay stored in USD so a project
opens identically for anyone, and convert on display; typed values convert
back. PropertyPanel's Cost field follows the same rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nting zeros

The budget target field gains a scale selector: type 7.05, pick Cr, get
₹7,05,00,000. Rupees offer — / K / Lakh / Cr; every other currency offers
— / K / M / B. A hint under the field spells out the expanded amount so there
is no ambiguity about how many zeros landed.

The scale is display-only — the budget is still stored in USD, so switching
Cr -> Lakh re-expresses 7.05 as 705 without changing the budget. The unit is
auto-picked from the stored value (the largest one that leaves a value >= 1,
i.e. what a person would say out loud) and re-picked when the currency changes,
since lakh/crore only apply to rupees.

Also adds stable testids to the currency and install-factor selects, since the
new scale select otherwise shifted what ".bd-controls select" first matched.

tests/model/currency.test.ts covers the conversion round-trips, the legacy bare
symbol docs ('₹' -> INR), unit sets per currency, auto-pick, and that 7.05 Cr
is exactly 70,500,000.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and range, and says when you override it

Dropping a component opened a Cost field whose researched price was only a
faint placeholder, so the number driving the estimate was invisible until you
clicked in. The field is now a small price block:

- the budgetary price spelled out under the input
- the size/rating that price assumes (the researched basis)
- the realistic low–high range for the component class
- an "est" badge where no vendor publishes a price and the default comes from a
  correlation or build-up rather than a listing

Three prices can apply to one component, most specific winning: its own cost,
the project-wide override from the Budget dialog, then the table default. The
note now names which one is in force — "$9,500 budgetary", "$7,000 project
price", or "Your price — overriding $9,500 budgetary" — so a number on the
drawing is never unexplained. Typing a price overrides; ↺ clears it and hands
the component back to the default.

Extracted the field into a CostField component so it owns its own store reads
instead of threading doc/setNodeCost down from NodeProps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and a workspace design pass

Fit was broken, and the cause was structural. The paper was created at the
SHEET's pixel size — 1587px for an A3, 3178px for an A1 — so
transformToFitContent fitted the drawing to the sheet, not to the window, and
whatever the panels left visible then clipped it. The paper is now a viewport:
it fills its container, tracks it with a ResizeObserver, and the sheet is drawn
as a white page on a grey desk inside the transformed layer group.

- fitView() measures the container's own client box, so it is automatically
  correct with the palette and properties panels open, closed or mid-resize.
  It fits the sheet UNIONED with the content bbox, so a symbol dragged off the
  page still lands on screen instead of silently outside it.
- The view re-fits itself when the container changes size, until the user pans
  or zooms; after that the view is theirs (viewState.userMoved). Fit claims it
  back. Opening a sheet, switching sheets and changing sheet size all re-fit.
- The canvas opens fitted instead of showing the top-left corner at 1:1.
- Shift+F fits, Shift+1 returns to 1:1, and the toolbar gained a live zoom
  readout that doubles as a reset-to-100% button.
- MIN_ZOOM drops to 0.05 so an A1 can actually be fitted in a laptop window.

Fixed a latent bug in the same area: the DXF underlay was inserted as a sibling
of .joint-layers rather than inside it, so it was drawn unscaled at the SVG
origin and desynced from the drawing as soon as you zoomed. Both the underlay
and the new sheet page now live inside the transformed group. The SVG/PNG
exports strip .pid-sheet alongside .pid-underlay, so the on-screen page and
grid never reach a file.

Design pass on the P&ID workspace (HMI untouched):
- one cool-neutral token ramp instead of scattered literals
- toolbar holds a single row from 1180px up, with consistent 26px control
  heights, square icon buttons and a segmented zoom cluster; labels drop at
  breakpoints rather than wrapping the row and shoving the canvas down
- titled, sticky SYMBOLS / PROPERTIES panel headers that align across the shell
- sheet tabs and drawer tabs read as real tabs joined to their surface
- status bar aligned with findings pushed right, and its unsaved-changes dot
  actually renders (it was emitting a literal ●)
- visible focus rings on toolbar, palette, tabs and panel inputs

e2e/interactions.spec.ts pinned the paper to 1:1 implicitly by relying on the
old default view; it now resets zoom before measuring an on-screen box.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Marquee-select a drawing, drag it, and the symbols moved while every
hand-routed line stayed behind — the waypoints kept their old coordinates, so
the routing sprayed across the sheet. moveNodes() only ever mapped over
sh.nodes; sh.edges was untouched. Both the drag path and the arrow-key nudge
funnel through it, so both were wrong.

A line whose every connected symbol is in the moving set travels rigidly with
them, so its vertices — and any free end, which no selection can contain —
translate too. A line with one end outside the selection is being stretched
instead, and its waypoints stay put; that is the correct deformation and moving
them would be wrong.

The live drag preview needed the same treatment, or the routing looked broken
until you released the mouse: vertices of links fully inside the drag set are
captured at grab and translated each frame. That write is deliberately
graph-only — change:vertices ignores programmatic updates that carry neither
`ui` nor `tool`, so the preview costs no history entries and cannot
double-apply against the store write on drop. Verified: dragging 17 symbols
with 38 waypoints translates everything by exactly the same delta, adds exactly
one undo step, and one undo restores the original geometry.

duplicateSelection() already offset vertices correctly, which is why paste kept
its routing while move did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Relicenses the project and adds the account layer the cloud features need.

Licensing

- Moves from AGPL-3.0-only to PolyForm Noncommercial 1.0.0: source-available,
  free for personal, academic, nonprofit and government use; commercial use
  requires a paid licence. Not retroactive — v0.12.1 and earlier remain AGPL
  for everyone who received them, and that grant is not revoked.
- Adds NOTICE, SECURITY.md, and SPDX headers across the source tree, with an
  @license banner preserved through minification so the terms travel with any
  copy of the bundle.

Accounts and cloud drawings

- Email/password and Google sign-in; the editor opens behind the gate.
- Drawings save to Firestore per account with debounced autosave, a size cap
  the deployed rules enforce, and readable messages for the failures a user can
  actually act on.
- A marketing homepage at /, the editor at /app, split so a homepage visitor
  does not download the editor.

Firebase config is read from the environment, with no hardcoded fallback

A Firebase web config is not a secret — it identifies the project to the client
and ships in the bundle by design, with access enforced by firestore.rules and
the authorized-domains list. It is env-only for a different reason: a hardcoded
default is inherited by every fork, so a fork's users would sign into THIS
project — real accounts and real drawings landing in someone else's Firestore,
against their quota, with them as the data controller.

Absent config is therefore the safe default. Without .env.local the app runs
fully local: draw, validate, export and save .pnid files, with the sign-in gate
and cloud drawings switched off. .env.example documents standing up your own
project, firestore.rules ships so a fork can reproduce the security model, and
.firebaserc is untracked so `firebase deploy` never targets this project by
accident.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The toolbar was full: about eighteen controls already scrolling horizontally,
with a source comment conceding it. There was nowhere to put a second screen.
This adds the top-level navigation the engineering-platform plan needs, and
improves getting around on its own merits.

Workspaces

- A rail down the left edge: Draw, Data, Checks, HMI. Ctrl+1..4 switches, and
  the Checks entry carries a badge only when there are findings — badges are
  the one thing allowed to pull attention out of Draw.
- Each workspace is a real route (/app/draw, /app/data, …) so it is linkable
  and survives the back button, and each is a lazy chunk so Draw does not pay
  for the report tables or the HMI simulator. Bare /app and older deep links
  still open the drawing.
- Draw itself is untouched: the same grid, one 48px strip to its left.

Command palette (Ctrl+K)

One way in for an app that now has more screens than a toolbar can hold. Jump
to any tag from any workspace, or type > for commands. Ctrl+F still opens it —
the find-a-tag habit is years old and there was no reason to break it.

Data and Checks

- Data shows the instrument index and line list on screen, not only in a
  downloaded CSV. Both are built from the SAME rows the CSV writers emit, so
  the screen and the report cannot disagree. Every row locates its object back
  on the sheet.
- Checks shows every finding and suggestion full-screen and grouped, instead of
  a 190px drawer, and can apply fixes.
- The inspector gains a Where used tab: the sheet, the rest of the loop, the
  connected lines, and the HMI screens showing this tag. All derived; every row
  is a jump.

Two measured fixes

- Cloud saves are about half the size. serializeDoc() pretty-printed and the
  cloud payload used that verbatim, so half the 900kB Firestore budget was
  indentation — 328 B/item pretty against 165 B/item compact on the 3-sheet
  refinery sample. The .pnid file stays readable; only the cloud copy is
  compact.
- Validation runs once per edit instead of three times. The status bar, the
  drawer and the validation panel each held their own useMemo(doc), so every
  keystroke ran the checks 3x and the suggestions 2x, each walking every sheet.
  They now share one pass, which also makes it impossible for two panels to
  disagree about what is wrong.

The drawer's Validation and Advisor tabs merge into one Issues tab; they were
always one engine split by an implicit severity flag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Engineering data finally has a home of its own, separate from the drawing that
displays it.

Until now it was scattered: node.datasheet for instruments only, node.config
for things that are really engineering attributes (a control valve's fail
position), edge.lineNumber for four strings, node.cost for a number. Nothing
existed for equipment specs, line specs, status or ownership. Worse, records
hung off node ids, so deleting and redrawing a symbol silently destroyed a
whole filled-in datasheet.

Records are keyed by tag, not node id

A tag is the engineering identity; a node is one placement of it. That matters
because the same tag legitimately appears twice (an off-page continuation, a
valve shown on two sheets), because delete-and-redraw is normal drafting, and
because every deliverable already joins on tag. The trade-off is deliberate: an
untagged object gets no record, and the advisor offers to tag it.

Four invariants, each covered by a test

- Renaming an object MOVES its record; if another symbol still wears the old
  tag it COPIES instead.
- Renaming onto a tag that already has a record REFUSES to merge and leaves
  both intact — merging two engineering records is unrecoverable, and the
  duplicate tag is already a finding to resolve.
- Deleting a symbol NEVER deletes its record. Deleting a symbol and discarding
  an approved datasheet are two different intentions and only one was
  expressed; the orphan is surfaced with a purge action instead.
- A pasted symbol arrives untagged and gets no copied record.

The Engineering tab

The record for the selected object, with a field catalog per kind: instruments
get calibrated range and signal, valves get trim, Cv and fail position,
equipment gets duty and construction, lines get pipe class, design conditions,
insulation and tracing. A fill meter shows how complete it is, and a status
runs draft / in review / approved / issued.

The datasheet form, the printed datasheet and the datasheet-matrix CSV are now
views of that one record rather than a second store.

schemaVersion 4 -> 5, non-destructively

Documents saved by this version cannot be opened by v0.14.0 or earlier; opening
older documents is unaffected, and v1 through v4 all migrate in. node.datasheet
is deprecated but still WRITTEN and still READ as a fallback for one release,
so a half-migrated document loses nothing and an older build keeps working. A
datasheet on an untagged symbol is parked under __unassigned:<nodeId> rather
than dropped — binning someone's filled-in datasheet because they never tagged
the symbol would be the worst possible outcome. The migration is idempotent and
will not clobber a later edit.

tests/model/examples.test.ts runs every bundled example — they span
schemaVersion 1 through 4 and are the Templates menu — through the migration,
checking the node count survives and the document round-trips. Every future
schema bump has to keep it passing.

Also removes PlantNode.attrs, declared since v1 and never read or written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Coldbari
Coldbari merged commit 0aae247 into main Sep 1, 2026
4 checks passed
@Coldbari
Coldbari deleted the release/v0.13.0-licensing branch September 2, 2026 06:17
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.

1 participant