five matching library files under the Library row — board number, file name
and folder, board or PDF — with a *N more…* spoiler for the rest. A click
opens the file; the Library row still opens the panel with the whole list.
copy and web search that searches the open PDF for the text under the
cursor.
with the selected part's name, *Find net* with a lit net's or a selected
pin's net — and the sidebar's Search tab follows whatever the ribbon field
holds. It used to send a selection to the linked schematic.
the query; otherwise the shortcut only focuses the field. It used to replace
the text with the board's selected part after the page had been dragged, and
re-ran the search, which jumped to another occurrence.
⌘D, ⌘P, ⌘O and the zoom keys never reach the browser, and a right-click on
the tab strip or any other chrome no longer opens its menu (text fields keep
it for paste).
100 %, the ribbon's suggestion list, the context menus, the resize popup,
the link dropdown and the toolbar tooltips opened away from where they
belong — at 125 % a quarter of the screen to the right. They now land on
their field or pointer at every scale.
pointer until the button is released, over the toolbar, the sidebar or
outside the window; a scroll over the ribbon, the minimap or the slide sheet
moves the board; and the minimap only drives the view for a press that began
on it — it fades in under the pan being made and used to jump the view when
the drag crossed it.
auto-switch that brings the linked board forward left the board as the
active panel, so the shortcut answered with the panel the user had not
clicked.
the start never registered its find field, so the shortcut set the text
without focusing it.
panel is the active one the board panel is paused (no frames are drawn),
so a mode change made there — or any other change from the store — showed
only after the next tap on the board. The board now draws the frames such a
change needs and pauses again; a lookup from the schematic no longer leaves
it running until the panel is next activated.
a pinch.** Pressing the net-lines button changed the mode but drew nothing
until the next re-selection, and *off* left the old lines on screen; the
next recompute (a rotation, a camera restore, on a desktop a keyboard or
drag zoom) then wiped them, and the button could not bring them back. With
rotation unlocked, any pinch whose fingers twisted past 2° blanked the lines
for the whole gesture. Now a press redraws immediately, *off* clears, and a
live two-finger turn carries the lines with the board.
its selection orange and adjacent nets in blue, Atari in its orange and blue;
the dark schemes keep yellow and blue. The line colours in Settings ▸ Net
Lines apply on BoardRipper Default, and the row says so on a scheme.
change in Settings ▸ *this board* while Dark, Landrex, Light or Atari was
active saved the whole scheme as that board's overrides, so the board kept
the scheme's colours under BoardRipper Default. Cancel and Preview also
dropped the scheme until the next board switch. A board's own keywords and
part types now survive a scheme, and a scheme is in effect from startup, not
from the first board open.
board**: its body is washed in the highlighter colour, not the ink. Pin
numbers and net names on ink-filled pins (the *Ink pins* styles) are drawn
in white so they stay readable; the highlighter halo stops short of
neighbouring pins on fine-pitch parts; the spotlight dim fades toward the
scheme's colour instead of black; the minimap follows edits to a custom
scheme.
about 4.35:1). The ◐ and filter menus open in the window the PDF panel is in
when it has been popped out.
colours follow.
(rotation unlock 24°, the Rendering tab, 15 formats, dev port 8082, the
licence lines, and what shipped differently from the scheme spec).
did not show on paper (and on Atari it was the rail colour itself). The pins
of the selected net now sit on a yellow highlighter halo with a thin ink ring
and keep their own colour. Settings ▸ Selection & Highlight ▸ *Selected pins
on light schemes* offers two more: *Ink pins* (solid ink with a white rim)
and *Ink on highlighter*. The dark schemes and BoardRipper Default are
unchanged.
Each of the four owns every colour on the board — pins, part bodies,
outlines, the selection, the dim and the copper:
grey, ground a dark teal, and colour only for what you select (yellow net,
red part).
board: black, an olive board edge, lavender part outlines, every pin the
same blue. Its old black-and-white look is gone.
pins.
taken from the five rainbow-stripe colours.
BoardRipper Default is unchanged and stays the default.
power and nothing else.
(a halo, or black/white text where the scheme's own would not read), the
boxes behind net names follow the scheme instead of always being black, and
the minimap is drawn in the scheme's colours.
beside it. Besides inverting, it can darken the paper while keeping red and
blue lines red and blue, or use exact background and text colours you
choose. Two sliders handle coloured lines: *Colour* (down to grey, i.e. your
text colour) and *Contrast* with *Auto*, which lifts pale lines so they stay
readable on the new background. *Auto* is on by default for every style,
including plain invert and night mode carried over from v0.49, so inverted
pages now show pale lines stronger than before. The old plain invert is Auto
off and Contrast at 0.
new ones. On the new schemes the dim appears only with a selection and fades
toward the background instead of black.
connector covering a third of the board is a tint, not a block.
that had one of them selected opens in Light. A custom scheme is kept as it
is.
zoom, the page is drawn at the preset's size; a moment after you stop, the
part on screen is drawn again at the exact zoom. Fastest is now as sharp as
Quality once the view settles — it was unreadable past 2×.
whether the file is downloading, being read, opened or drawn; a file that
cannot be opened says why. The banner over the board area covers PDFs too,
and text indexing shows as "Indexing text 23 %".
names: Quality, Balanced, Fast, Fastest.
page. Fast and Fastest trade resolution, frame rate and trace corners for
speed on tablets.
board text renderer and diagnostics. It was System ▸ Performance & Debug.
Smooth wheel zoom and two-finger rotation moved to Input ▸ Navigation.
stored setting that left the board blank. Recent files, favourites, history,
the session and PDF bookmarks are kept.
eight-step zoom settles in about 3 s instead of 6, and the page freezes for
well under a second instead of three. Neighbouring pages are drawn only as
sharp as they need to be, the same pixels are no longer rendered twice, and
the pages you are reading stay ready instead of being re-read after four
seconds.
off screen, and empty part containers cost nothing.
a selection or a rotation was redrawing every panel.
the page into a canvas of about 700 MB next to a neighbour of the same size,
more than Chrome's graphics memory. Canvas memory now stays near one screen.
the fingers stop.
no pin numbers or net names.
first page about 300 ms sooner.
indiafix watermark is filtered by default.what the board file knows beyond its geometry. Today that is XZZ's
"Common problems" boards: the repair table drawn on the board
(symptom, fault point, fix) and the net glossary — what each net does,
filterable, with the highlighted net's description at the top. Part and
net names in it are clickable. The tab appears only on boards that carry
notes, and it is plain text, so the browser's *Translate page* can
translate the Chinese.
selectable and part of their net — on the iPad Air 3 boards a net that
showed only a BGA ball now shows its probe point too. Records that only
repeat an existing pin are left out.
files found sits above the tree; a click opens the branch, selects the
file and scrolls to it. A folder with a hit also shows its other files
(the schematic next to a board, say), dimmed, behind a toggle when there
are more than five.
icon buttons beside the name, one line says what the file is, and the
rest (facts, path, *Edit metadata*, *Mark as donor*) is behind
*Details*. OpenBoardData takes two lines. The pane scrolls under a fixed
header and takes at most 45 % of the Library.
U1000:none before, over a thousand now), files carrying both reading formats
keep both, and renamed parts keep theirs. Across the local test library
the number of readings roughly doubled.
read correctly instead of being dropped; Ω values show.
the whole window when switching board tabs. Fixed throughout, with a
recovery page for any crash outside the panels.
90° turn rebuilt the whole scene. It now only moves the labels that
depend on the turn.
because the flip's own work made the press look long.
statistics) or fail the library load (donor list).
as a PNG — for a repair ticket, a message to a colleague, a lesson. Draw
arrows, boxes, freehand with a pen, and text in five colours and three
weights; everything stays pinned to the parts while you pan, zoom or
rotate. Every selected part gets a large label with its name, and
⌘+click (Ctrl+click on Windows, or the *Several parts* toggle on a
tablet) labels several at once; under the label of the part you picked
last are two boxes to note an Ω and a diode reading, which print on the
label. The PNG carries the board file name and the date in a header, the
labels, your annotations and the minimap. Copy puts the same picture on
the clipboard. A guide card explains the tool in hand; Esc leaves, and
the annotations come back when you return.
eighth of the screen fades in a small map of the whole board at the top
with a box for what you are looking at — click or drag on it to move.
It leaves when panning stops, stays up in slide mode, and the ribbon
button turns it off.
with the watermark text filter and its terms, *Strip small watermark
images* (which had no control before) and *Skip invisible content*.
The two parse-time options show a *Reload & re-parse* row when an open
PDF was opened under other values; page, search and extracted text
survive the reload.
a second one to open its menu. Any touch outside closes it.
a page-sized invisible overlay that pdf.js still decoded, rasterised and
composited on every render — 100–190 ms per render on an iPad. It is
dropped before it reaches the renderer; the picture is pixel-identical.
pixels already existed, the viewer put the blurry preview on top of
them for a moment, and in Safari a cleared tile could reach the screen
for a frame. Both causes are gone.
active panel, a wheel over the board took the zoom and drew nothing. A
regression from July.
boards; it now says the server was updated and offers a reload.
are back.
a zoom change; a failed strip left a permanent "reload needed"; a term
typed in the filter menu and clicked away from was lost.
⌘] forward again (Ctrl+[ / Ctrl+] on Windows and Linux) — across boards
and PDFs alike. A visit is a part or pin you clicked, a PDF lookup in
either direction, or a search and the result you picked from it. The
view comes back exactly as you left it, not re-fitted. Going back and
then somewhere new deletes nothing: the path you left is still there.
The board ribbon has ‹ › buttons for touch screens; hold one for a list
of where it leads.
by file. Click a row to go there. Filter by kind — clicks, PDF lookups,
searches, viewpoints — and by boards, PDFs or just the board in front of
you. A row for a file you closed reopens it from the library. The
history survives a reload.
places you looked at with nothing selected are kept too: a region of the
board, a PDF page, another board tab. Only a place you stayed on for two
seconds counts, so paging through a document leaves one row.
your recent searches when you click into them; typing narrows the list,
↑↓ and Enter pick one.
Enter or ⌘F after panning away did not bring it back, and a drag that
was still gliding pulled every jump off its target.
board about the point between them, live. Rotation starts locked: a turn
under 12° does nothing (every pinch turns a little), a turn past it
unlocks — a toast says so — and the board turns on from there. The angle
is kept when the fingers lift, snapped to the nearest right angle when
within 10°; the rotate buttons step to the next right angle from any
angle. A double-tap with two fingers locks rotation again and squares the
board. Settings ▸ Performance & Debug ▸ Two-finger rotation turns it off,
and the unlock angle and the snap window are sliders there (a snap window
of 0 never snaps). In Butterfly the whole spread turns as one picture.
browser turns Shift+wheel into a horizontal scroll before the page sees
it, and both viewers took it for zoom. The three scroll slots — plain,
Shift, ⌘ — are now independent, for the board and the PDF alike, in
Settings ▸ Input and on the start page. A fresh install gets Shift = Pan,
⌘ = Zoom; an existing install keeps the layout it had, and the start page
offers the one-click switch.
boards showed the button and it did nothing. Traces, vias and copper
pours are hidden while Butterfly is on — they belong to the board as a
whole, not to a side — and a toast says so.
freezes the page while its scene is built — a second or more on a
tablet, during which a finger on the glass gets no answer at all. A
"Loading …" banner now sits at the top of the board area from the moment
the file is opened until the new board is on screen, and it is painted
before the freeze begins.
smoothing (MSAA), on by default. Off saves GPU work on a machine that
cannot afford it, at the price of jagged thin lines.
build at ripperdoc.de/boardripper/legacy-web, and a supported-devices
page lists browsers, iPads and iPhones by model. A browser below the
floor now sees a message with the minimum versions instead of a blank
page.
sub-variant (Jasper_Kronos) drove a rewrite of the pad walk. Pads come
from the pad ring with the pin numbers the file carries — the old walk
invented 1..n in file order, which swapped pins 1↔3 on every SOT-23 and
1↔2 on every diode — and drawing symbols no longer become empty parts
that blew up the board bounds. Against Allegro's own v17 re-save of the
same board: 7982 of 7982 pins, 7910 nets agree, none disagree.
URLs pdf.js was given for its character maps, standard fonts and wasm
decoders pointed at nothing in every build; only the dev server worked.
PDFs with non-embedded fonts or JPEG 2000 / JBIG2 images render now.
on, a tap while the board was still gliding stopped the glide but selected
nothing, which read as "sometimes a tap can't select a component". A tap
during the glide now selects the part under it.
used to answer the same hold with its own callout and cancel the touch.
The first board opened on a touch screen says so once.
turned off edge smoothing for the board, which left every thin line —
the board outline first of all — jagged and at an uneven width. Edge
smoothing stays on; the profile keeps its 60 FPS cap and 1.5× render
resolution ceiling.
bottom silkscreen out of the shared silkscreen layer to mirror it with the
bottom half, and the toggle stopped reaching it there — the silk came on
with Butterfly and could not be turned off.
is cleared every frame, so it never showed anything.
seconds on a tablet.** After a tap lookup the board used to redraw every
pin of the lit net thirteen times over three seconds — to blink the outline
of the selected part, which is the only thing the blink changes — and force
a full redraw of the whole board each time. On a large board that was the
5–10 s of unresponsive pan and zoom after tapping a reference on an iPad. A
net lookup no longer blinks at all (there is nothing to blink), and on a
board where one redraw is already expensive the part lookup flashes once.
once and pulsed, instead of being rebuilt on every frame.
for an update when the page was loaded fresh, which an iPad tab or a
home-screen app almost never is — one was still running 0.40 three releases
later. It now checks every time you come back to it, when the connection
returns, and once an hour, and offers the usual *Reload*. A device already
stuck on an old version needs one real reload (or a full quit and relaunch
of the home-screen app) to pick this up; after that it stays current.
few zooms, panning only worked with two fingers. When the tablet's own
gesture recogniser claimed a pinch before the second finger had registered,
the board paused its one-finger drag for the gesture and never resumed it.
It now resumes the moment the last finger leaves, whichever path handled
the gesture.
server down or restarting, the Library said "Library is empty — no files
indexed yet" and offered the scan button for as long as the request hung,
then switched to the unreachable notice. It now says it is loading until
the server has answered, and "empty" only once the file list has actually
come back empty.
promise a retry that never happened; a reload was the only way back. The
app now retries on its own, at 2, 4, 8, 15 and then every 30 seconds, shows
the current interval, and offers a *Retry now* button. When the server
comes back the library loads without a page reload.
fine, now says so and offers a Reload instead of either a spinner or a
false "empty".
behind it, one transition per load — are in CLAUDE.md under status
messages, with a spec that samples the panel over time through three ways a
backend is unreachable in the field.
side in the PDF search bar): docs/specs/2026-09-22-pdf-search-slots-mockup.html.
Docker installs were affected: any container on a user-defined network,
which is every docker compose up and every Synology Container Manager
*project*. A plain docker run on the default bridge always updated fine.
The health check that decides whether a new version is alive ran from a
helper container on the default bridge, and Docker drops traffic between
two bridge networks, so the check timed out and the update was undone
every time. The helper now joins the app's own networks and tries every
address that could answer: the container's IP, the host at the published
port, and the container name.
update-orchestrator.log in the data directory, and a rollback keeps the
failed container as <name>-failed instead of deleting it, so its log can
be read afterwards.
back cannot update itself onto this fix, because the drop-to-update bundle
uses the same helper. Pull the image once by hand — `docker compose pull
&& docker compose up -d`, or Container Manager ▸ project ▸ Build — and
in-app updates work from then on.
Every browser on iPadOS is WebKit, Chrome included, so these apply to all of
them.
under the first finger, jump, and land somewhere other than where the
fingers were. iPadOS hands a two-finger gesture to its own recogniser and
cancels the first touch before the second one arrives, which the board
read as a tap; and both the board and the PDF viewer ran two zoom
handlers on the same gesture — one for trackpad gestures, one for fingers.
One handler owns a gesture now, a cancelled touch is never a tap, and the
zoom follows finger distance exactly: the same spread gives the same zoom
however fast the fingers move, and pinching in then out lands back where
it started.
the other and the page used to slide away from the finger that never
moved, by up to 140 px. The zoom is now anchored on the moving midpoint
in both viewers.
still, so the board allows 12 px of travel for a touch against 5 px for a
mouse, measured as a straight line rather than the sum of the two axes,
which had rejected diagonal taps. A part that stopped responding to taps
after being deselected from the net list or a worklist responds again.
the page used to keep going onto blank paper; it now flips pages, and so
does a flick. Flick inertia had never actually run, on any platform. A
gentle flick carries a few pages, a hard one reaches the end of the
document.
second visible page did nothing on the iPad, because that page was not a
touch target. One surface now takes every touch on the panel.
while pinching, as one does when holding a tablet, matched neither the
pan nor the pinch path.
pinch and halved the frame rate on a large board. A single resting finger
still reads a pin's net and diode value.
without multisampling and at a capped render resolution; text keeps full
resolution. The PDF viewer defaults to the *medium* quality preset and
halves its cache budgets where the browser does not report device memory,
which is every iPad. Settings ▸ Performance & Debug ▸ *Touch performance
mode*. An iPad with a keyboard and trackpad, which reports a mouse-like
pointer, is detected too. No effect on a desktop machine: wheel zoom,
hover and clicking are measured unchanged.
the page from 100 % to 739 %, because the handler took the event for a
trackpad pinch. One notch is now about 2.4×, the same as on the board.
single-file offline build had no library at all, only the file picker.
Choose a folder, or drop one on the window, and it is indexed in the
browser: the Library panel, the Board# and Folders trees, recents and
worklists all work as they do with a server. Nothing is uploaded anywhere.
remembered; opening a file after a reload asks the browser for access
again, in the same click, and "Allow on every visit" or installing the
app makes that permanent. Firefox, Safari and the iPad cannot remember a
folder, so there the list comes back and the folder is asked for on the
first open. A board opened once reopens from the cache with no folder
dialog at all.
the home folder, Desktop, Documents and system folders without a word of
its own; the app now names them. A folder with no boards or PDFs says so.
.brd file was labelled BDV, because three formats claim the extension.
Ambiguous extensions are settled by reading the file header.
scripts/devdeploy.sh builds the working tree and ships it to the NAS dev container at rd-nas:1234, the touch-screen test target; --test runs the
WebKit iPad suite against it.
npm run test:webkit runs the touch specs on WebKit with an iPaddescriptor. Every board PDF in the repo is proprietary, so the PDF specs
had been skipping on a clean checkout; two generated PDF fixtures make
them run.
UPDATER_DOCKER_E2E=1 go test ./updater/ exercises the update helperagainst a real Docker daemon in three topologies: Compose network, host
network, and a new image that never serves.
one line naming the open worklist. Click it and you get a cursor: the
worklists you touched most recently are listed, and typing searches them,
across other boards as well as this one. New worklist, Paste a worklist and
All worklists sit underneath.
worklist takes the name you typed in the search, or opens for renaming if
you typed nothing. Clearing and deleting ask in the panel itself.
boxes whether or not anything had been measured, so ten nets were twenty
lines of which ten were empty. A net now shows only the readings it holds,
and the empty boxes open on the row you have selected, which is where you
are about to measure something. Click away and they fold back.
colours that were not the app's, so it stayed dark while everything else
changed.
you could not act on. Rows now say how much is in a worklist, how much of it
is marked done and when you last touched it, and a worklist on the open
board switches to it. One belonging to another board says which board.
flags, the reference, the note, remove — all of it, in the same order.
values, part names, type colours, dashed net lines and the HDR outline each
used to be a toggle plus a separate size slider (some of the sizes only in
Interactive mode). Each is now one slider: click the thumb or the row's name
to turn the thing off — the row greys out and keeps its value — drag to set
the size. Plain sliders look the same; only the on/off ones react to a click.
Same control in Settings and in the Interactive-mode popups.
of the board says so and carries a *Done* button — the mode repurposes a
plain click, so the switch that ends it sits where the clicks happen, not in
Settings.
corner cell that continues into the activity rail below it, the global search
spans the bar from top edge to bottom edge, and the bar's icons are one size.
The board panel's own sidebar follows: its tabs and the ☰ toggle are one
flat 40 px row with the same darker corner cell, instead of a floating chip
over the tab strip.
one of its pin labels to the "Selected Part Labels" size, and on a dense part
they overlapped each other. The selected part's pin labels now sit at 0.8 of
that size; the selected pin, the pin under the pointer (on any part) and the
part name keep the full size. The fraction is adjustable — Settings ▸ Zoom
Level of Detail ▸ *Other Pins of Selected Part*, also in Interactive mode.
Zoom Level of Detail) defaults to 8 px instead of 3 — at 3 the names showed
at half zoom, unreadable and covering the whole chip. An install that never
touched the value gets the new one; a changed value is left alone.
pin and cannot overlap, so it now has its own, lower appear-floor ("Pin
Numbers", 5 px) and is drawn centred in the pin until the net name shows —
then it moves aside to make room, on the same zoom step the name appears.
name is capped at the size that fits between the part's pin centres, so a
selected BGA's labels cannot overlap; a label that would have to go below its
appear-floor to fit is hidden. The pin you point at is exempt. Settings ▸
Zoom Level of Detail ▸ *Fit pin labels to pitch*.
labels swell into view past their appear-floor — the same mechanism as the
part-name fade, at the small end. *Label Fade-In* in the same section; 0
restores the pop.
so they had been failing since the icon rail replaced it.
emulation. Its output is the same JavaScript for every target, and the
emulated path was a coin flip: esbuild's install step runs its own native
binary to check it, and that segfaults under QEMU — it failed this release
once on dependencies that had not changed since the last one. Multi-arch
builds are also about six times faster for that stage.
after a scan started and the panel still waited for the scan to end — five
minutes of an empty list on a 60,000-file library. The list now refreshes
during the scan (5, 10, 20, 40 seconds in, then every minute).
anything until someone found *Scan now* in Settings. The first visit now
shows what is mounted, explains the two indexes — files now, PDF text in the
background afterwards — and starts the scan. Two options, both on: link
boards to their PDFs automatically, and download the OpenBoardData readings.
A database reset reloads the page and the setup comes back.
the port closed for ten seconds on every start.
name; a similar name (at least 75 % of the shorter name's words, within the
same folder by default); the only PDF in the folder (by default only when it
holds one board too). Settings ▸ Library shows the ladder with a Preview
— how many links each rule would make, with examples — before anything is
written, and every link remembers its rule so the links one rule made can be
removed together.
which is why it was off by default. Rules 1–2 are lookups and rules 3–4 only
look near the board's folder, so it can be on.
every PDF in the library; with 7,500 it blocked for a quarter second to open
and on every keystroke. It shows 200 and the filter narrows the rest.
couple of minutes — so readings are already there when a board is opened.
only by a number in the filename.
every pin that is not a plain match — red where the boards disagree, amber
where the net is the same one named differently. Plain matches stay unmarked
so the ten pins that matter are not buried among the 390 that agree.
name and its net for that pin, coloured the same way as the mark.
labels the pin's state and is stripped before names are compared, so
NC_GPU_TRIGGER1_L and RSVD_GPU_TRIGGER1_L read as one signal. Two no-connects
that carry different names are shown as a partial match, not as "no
difference" — the pin may have been repurposed between models.
against PP3V8_AON_MPMU_ISNS_VIN), and a rail's fanout no longer counts as
evidence against a match — across two boards it differs by design.
identical file already in the library is reported as existing; a same-named
file with different bytes is saved as "name (n)", never overwritten. Dropped
PDFs are indexed at once instead of waiting for the next sweep, and the
Library merges the new rows instead of re-streaming the whole list.
lives in src/backend; the Vite proxy targets port 1336), the Synology
docker run carries --user 0:0 and the incoming mount, the tarball name
matches what the release script produces.
by pin. Pick a board and a component on each side. The two sides do not have
to carry the same name — a chip can sit at U1700 on one board and U6800 on
another, and comparing those is the interesting case once you leave one board
family.
lists the other open boards that carry the same name and fills both sides in
with one click. "Compare with…" fills in only this side, for when the other
board calls the part something else.
often disagree only about naming — one carries PPBUS_G3H where the other
carries N$21004. When the names differ, BoardRipper looks at which components
each net actually touches. The same components means the net was renamed, and
the row says so instead of reading as a fault. Rows where the wiring itself
differs are marked separately, so a board full of renames does not bury the
one pin that changed.
two share marked in both. PPBUS_G3H against PPBUS_G3H_R is one rail written
two ways, and marking what they share leaves your eye on the bit that
differs. This only applies where one name contains the other, or where they
differ only in punctuation — SMC_RST_L and SMC_RST_R are two different nets,
not one, and are still shown as a difference.
any pin where the two readings drift apart. A pin that agrees on its net but
disagrees on its reading is usually the one you were looking for.
number pins in the order they happen to appear in the file rather than by the
pin on the package, so two files of the same board can number the same pads
differently. BoardRipper tries the pad names, the numbers, the file order and
the pad positions, and keeps whichever best explains the nets and the copper.
It shows which one it used and how well it fit, and you can pick one yourself.
mirrored. When the net names give no clue which way round the part sits, the
pairing is a guess, and the tool says so rather than presenting it as fact.
It also warns when the two footprints are too different in size to be the
same package.
one comparison you can mean. The components stay blank — which chip to look
at is the actual question.
on the clipboard. Widen the sidebar and the two sides get their own columns.
pixels off its own grid, which was small but hard to unsee. It now shares
the column's width and icon size, so the two read as one edge.
had to hover to learn.
version and the word Update, so it reads as something to do rather than a
number that changed. The badge keeps its width while it swaps, and with
reduced motion it simply says Update.
column as a pin, above the status bar button: pinned means the panel stays
in the layout, a crossed-out pin means it floats over the board and leaves
when you click into it. It was only in the right-click menu before, which is
not where anyone looks for a mode. The menu item and the setting still work
and show the same state.
not what a click would do. Hovering used to show the opposite of the current
setting, which read like a description of how things already were.
the board, but it was also landing as a shift+click on whatever sat under the
cursor, so zooming quietly filled the worklist with parts you never picked.
Shift+click still adds and removes as before.
setPointerCapture on thecontainer, which retargets pointer events away from the canvas. PixiJS
listens on the canvas, so it decides the pointer left and emits
pointerupoutside; pixi-viewport reads that as the end of a gesture it
believes never moved and fires clicked during the drag, one move in.
The release-time latch that was supposed to swallow it therefore ran too
late — and stayed armed, eating the *next* real click. Fixed with an
in-gesture flag, a pointer-travel test for clicks emitted at release, and by
clearing the latch on pointerdown. Regression: tests/shift-drag-worklist.spec.ts.
store/part-compare.ts — no React,no stores — so it can back an MCP tool later without a browser. The tool
reads the raw parse, never the derived view: deriveBoardView filters parts
and rebuilds nets, which would let a view filter shrink a topology
fingerprint and turn a rename into a difference.
ToolsPanel's open tool moved out of local state into panels/tools/tools-nav.ts so the right-click can navigate to a tool.
boardStore.selectPinInTab() selects on a board tab that is not the active one. _resolveAdjacentNets was split into a tab-scoped _adjacentNetsFor —
the old form resolved the chain against the active tab's netlist, which was
unreachable before and wrong for a background tab.
data-testid="part-compare" and its rows by compare-row + data-status.
docs/specs/2026-09-11-part-pin-comparison-design.md.keeps a parsed copy of every board it has opened, and it was handing back
the old copy instead of reading the file again. So if you had already opened
an Allegro board, nothing changed for you in 0.39.1 and there was no sign
anything was wrong. Boards are re-read once after this update.
planes — were never read, so Allegro boards showed bare traces over an empty
board. Turn them on with "Copper fills" in the Layers panel.
files describe a part in full — its name, its type, every pin and net — but
never say where it sits. On one Lenovo board that is 67 parts, among them the
battery charger, the graphics power controller, both memory sockets and every
edge connector. Cadence's own viewer does not show them either, so on a board
like that the parts you most want to probe were the ones nothing could point
at.
BoardRipper now finds them by following their own copper: a track that ran to
a missing pad still stops where that pad was. 23 of the 67 come back this
way, each pad within a thousandth of an inch of where it belongs. They are
drawn with a dashed amber outline, because their position is worked out, not
read from the file, and you should always be able to tell the difference.
Parts with too little copper to go on — mounting holes, mostly — are left
out rather than guessed at.
Allegro board units byte" message. The file's version line sits in one of
two places depending on which Allegro 18 wrote it, and BoardRipper only
looked in one of them. It now finds the line in either place, so these
boards open. Seen on Compal LA-P161P.
their copper layers from zero, both sides landed on the same layer, so the
Layers panel could not tell them apart and every trace was drawn in one
colour. Each side now gets its own layer, with its own colour, and can be
turned on and off on its own. Which side is which was checked against where
the traces actually meet their pins.
(Top / Bottom), Butterfly, Rotate left / right, Traces and the rarer
transforms (180°, mirror, flip axis, under ⋯) moved from the top bar into
the board's own control bar, where they lead the row. They act exactly as
before, with the same keyboard shortcuts. The top bar keeps the sidebar
button, Open, search, 2-window mode, About and the version. Search sits in
the centre and says what it searches.
front of your arrangement, not inside it. Hide any of them with a
right-click on the bar.
Left, Centred or Floating (then drag it by its handle to wherever it does
not cover the part you are working on), and Orientation Horizontal or
Vertical. Both are remembered.
magnifier with a chip or a net inside, so the column stays one button
wide. Click one and the field pops out beside the column; Escape or a
choice folds it back. The horizontal bar keeps the plain fields.
under the field, and opens upward when there is more room above.
iMacs the two halves of the board are drawn edge to edge, so the file holds
them as a single shape and parts run right up to the seam. BoardRipper read
that as one board, so "Folded" and "Show all sides" were two names for the
same picture and an edge appeared to run from one half into its mirror
image. These files now split at the seam and fold like every other board.
around it. Both halves running the same way means the bottom is mirrored
across the seam. Opposite ways means one half was drawn as seen through the
board, and the bottom is moved onto the top instead.
recognised and left alone.
fold.
version.** Its light tiles were fetched from a fixed address that only
exists on the Docker install, so on the desktop app, the hosted browser
version and the offline single file the outline quietly drew nothing.
Butterfly on, a scene rebuild destroyed the dim layer that shades the
unselected half and re-added the dead object next time, which stopped the
render ticker; every button then looked fine but nothing on the board
responded. The layer is now detached before the rebuild like its siblings.
Moving, folding or re-orienting the board bar also no longer rebuilds the
scene at all: those settings are pure layout.
below the tab strip, so you can switch boards while it is open, and in a
split layout it stops above the lower group's tabs as well.
where the Classic Mac collapse box was, rolls the whole bar up into itself
and the board gets the room. Click the handle again to bring it back. The
choice is remembered.
the tab's name under the icon when you hover, the same way the rail does.
any button, put the row on the left or in the centre, fold it, or reset it.
Settings ▸ Board ▸ Board overlay is now one list with names: an eye to show
or hide, arrows to reorder, and a × on separators. New buttons added in
later versions land where they belong in the row, not at the end.
Revisions and Worklist are now the same icon cells as the Library and
Settings tabs: icons stay where they are, the open tab's name sits
underneath.
you opened the panel and was not useful. The ☰ now only opens and closes
the panel.
icons — Library and Tools at the top, Debug and Settings at the bottom —
stays on screen whether the panel is open or not. Click an icon to open it,
click the active one to hide the panel. The rail shows what is waiting for
you: new errors in the log, a failed library sync, an available update.
only, and nothing at all. The last state hides the status bar too and
brings the small arrow at the screen edge back as the way home. The update
badge and Library links reopen the panel from any state.
position, expanded folders, search text and the tool you were in. Whether
the sidebar was open, and on which tab, now survives a reload.
navigation. The panel opens over the board instead of pushing it aside, and
hides again when you click into the board. The board never moves.
rail, in the status bar's own row.
Text tabs. Side, captions and auto-hide are set there as well, for touch
screens that have no right-click.
tab's name sits under its icon. Before, Settings put the name next to the
icon and pushed the other tabs along.
data-sidebar-tab, data-library-tab and data-settings-tab, never by visible text.
docs/specs/2026-09-02-activity-rail-design.md, plan docs/plans/2026-09-07-activity-rail.md.
outline keeps a minimum size on screen (Settings ▸ Selection, default 24
px) so a small part does not shrink to a dot. Zoomed in, it sits exactly on
the part outline. The line is a fixed width on screen (the selection width
setting, in pixels now) and no longer grows into a band as you zoom in.
The outline is redrawn on every frame the zoom changes, so it does not lag
behind the board, and zooming with a large net lit costs the same as with
nothing selected.
highlighted net, search hits and the ghost outlines keep the fixed padding
and width from Settings.
selection outline hugs the pads. The gap that stayed even at 0 padding is
gone.
above that green, and OL blue. The colours are set in Settings ▸ Diode
readings.
is never dimmed. Pin numbers, net names and single-pin designators stay
hidden.
results, the net list and the info panel aimed the camera at the part's
position in the file, not where it is drawn after the board is folded, so
on iPhone files the view flew off the board or showed the wrong half.
and bottom side by side, and iPhone files pack two or three boards into one
file. The parser now finds the boards, pairs the halves and folds each
board, including pads, silkscreen, traces, vias and test pads. Before, only
parts, traces and the outline were folded when you picked a board, and a
notch or a cutout in the outline could break the pairing (iPhone 5, XS Max,
6/6s/8) or fold a file across two physical boards (XR, iPhone SE).
sides visible. The folded view is one click away in the board panel, and
that is where "Swap sides" acts.
traces, otherwise from the exporter's layout rule. Checked on the whole
corpus. The old rule (the part with the most pins is on top) was a coin flip
on iPhone boards and is no longer used.
board.
model, not by 820-number, and 124 of the 213 iPhone files in the corpus
landed in "Unrecognized". The scanner now reads the model from the folder or
the file name and groups the files under it. A model from the path beats the
"unsorted-apple" placeholder in the board database, and an 820-number in the
name no longer reads as an iPhone generation. Already-scanned files are
re-resolved on the next scan.
outlines (XZZ iPhone files, TVW, Allegro) the yellow box around a part on
the highlighted net sat a full pad outside the part border, and the gap did
not follow the selection padding setting. Dense parts (BGA, fine-pitch QFN)
had a smaller version of the same gap. Border, selection, hit box and halo
now use one rule.
It is always shown, with the detection result on the right and a small
mockup of the outline with and without HDR. The "Your monitor supports HDR"
prompt is gone.
and wrapped list items and bold code rendered broken.
menu, with the latest version line.
A release for the browser version and for tablets. The browser build at
ripperdoc.de/boardripper/web has been picking up users, and on an iPad it had
one bug that blocked it outright and several that made it feel unfinished. All
of the touch work applies equally to a Docker install opened on a tablet.
acceptextension to a system type; none of the boardview extensions are registered
on iOS, so the Files app greyed out every board. Browsers no longer get an
accept filter at all — the format is detected from the file's contents
anyway. Select a board and its schematic together and both open.
did not exist on touch. Hold half a second on a part or pin.
Upload button, the only way in on a tablet, included. On touch devices the
toolbar is now 60 px with 44 px buttons; mouse layouts are unchanged.
but not a locally-opened PDF, and iPadOS discards background tabs often. PDFs
opened from the picker are now kept (up to 6 / 256 MB) and come back with the
board.
window mode" is hidden where a pop-out window cannot exist (touch, and the
offline single file).
of the start page. The sample is a real routed board (Tomu FPGA, KiCad,
CC-BY-SA 4.0 — see THIRD_PARTY.md). The Docker and Library sections of the
getting-started text are no longer shown in the browser build, which has
neither.
icon instead of a page screenshot, and the install manifest carries the PNG
sizes Chrome expects. Installing also exempts the app from Safari's seven-day
storage cleanup, so settings and cached boards persist.
next load, which could also break a page that was open across the deploy.
Now the app shows "A new BoardRipper version is ready" with a Reload button,
and the swap happens when you reload.
glyph-overlay font parser load on first use instead of on every visit.
Now once, minified: 2.2 MB → 1.2 MB, and the offline precache 7.0 → 5.2 MB.
thread, so a large Allegro or Altium board no longer freezes the tab.
unsafe-eval.now hidden along with pin numbers and net names. Designators on part bodies
stay.
Diode-mode reference readings now come through on the current generation of XZZ
boardviews, and there is a way to actually read them on a dense board.
.pcb: diode values on newer boardviews. XZZ stores its diode table after the v6v6555v6v6 marker, and BoardRipper only understood the older of
the two encodings that live there — newline-delimited =<mV>=PART(pin)
records. Current deliveries (the iPhone-era files) put a JSON document there
instead, so every one of them reported no readings at all. Both encodings are
now read, chosen by content rather than by file name.
On iPhone16_16Plus AP+BB Boardview.pcb that is 4750 readings where there
were none, landing on 4195 pins. Its sibling …AP+BB YiDianTong.pcb is the
same board delivered without diode data, and still correctly shows none —
worth knowing if you have both, because the difference is in the file, not in
the viewer.
The JSON tail also carries the rename tables XZZ's own viewer applies, so two
further things follow. Pin identifiers are no longer restricted to digits,
which means BGA readings (M7, L9) can be represented at all. And parts a
file named by internal id (C356_1) now show their real designator
(C11814) — 2297 of them on the YiDianTong file. Net names get the same
treatment where a file provides them. Neither rename will ever merge two
parts, or two nets, into one name.
Spec: docs/formats/XZZ_FORMAT.md.
in the sidebar's View tab and in Settings ▸ Board ▸ Pins. On a dense part the
net name is usually what is covering everything else.
state: off → on → *only*. In "only", a pin shows its reading and nothing
else — no pin number, no net name, and no designator for single-pin
testpoints, which are drawn directly on the pin and were the worst of the
clutter (half the parts on an iPhone board are single-pin). Designators on
part bodies stay, so the board is still navigable.
Your own pin-number and net-name settings are left untouched while the mode
is on and come back when you leave it. It is also considerably faster: on a
dense area of iPhone16_16Plus the label count falls from 23 182 to 6535.
Two new board formats, both open and both with public test boards — the first
formats in this project whose parsers can be tested by anyone. Plus a fallback
for update checks when ripperdoc.de is unreachable.
.kicad_pcb (KiCad 4–9). Parts, pins, nets, board outline,tracks, vias and copper pours. An original implementation from KiCad's
public file-format documentation — the only openly specified format
BoardRipper reads. Two conventions were measured rather than assumed: the
rotation sign (KiCad angles are CCW as displayed over a Y-down frame) and
the fact that back-side footprints are stored already mirrored. Both were
settled by placing every pad under each candidate and requiring a track
endpoint at that spot to carry the pad's net — a position-only check cannot
tell the conventions apart on a symmetric two-pin part. Copper pours use
KiCad's own computed fills, so there is no fill algorithm; keepout areas are
recognised as not-copper and skipped. Spec: docs/formats/KICAD_PCB_FORMAT.md,
including a checklist for tools that write files BoardRipper reads.
.brd (Autodesk / CadSoft EAGLE 6.0+ XML). The third format to claim .brd; content sniffing keeps it apart from Apple BRD and Allegro. Pin
positions are not stored in the file — each part references a library
package, and every pin is the package pad run through the element's
mirror-then-rotate placement. Verified by a routing oracle: on the fully
routed test boards, 100% of pins land exactly on a trace endpoint of their
own net. Pre-6.0 binary files are rejected with a message to re-save from
EAGLE 6 or newer. Spec: docs/formats/EAGLE_BRD_FORMAT.md.
several closed contours — perimeter plus slots and milled windows — and they
were being welded into one run with a false edge leaping across the board.
Contours are now chained with a join tolerance and separated, and the
renderer punches inner contours as holes instead of painting them.
manifest had exactly one source, so any outage of that host stopped update
checks outright — and, until v0.36.0, stopped them *silently*. Observed on
2026-09-01: an install could not open a TCP connection to ripperdoc.de on any
port while github.com and ghcr.io answered normally from the same host.
Releases now publish manifest.json, its signature and the tarball as GitHub
Release assets, and builds carry a second source pointing at the stable
releases/latest/download alias. Whichever source answers first with a
signature that verifies wins.
This grants GitHub no additional trust. The manifest is signed either way, so
a tampered copy from either host fails VerifyManifestAny identically — the
fallback adds availability, not authority. GHCR is deliberately not a manifest
source: it is an OCI registry and cannot serve /manifest.json, though it
remains where the image is pulled from by digest.
The tarball now also lists the GitHub asset in url_mirrors, a field Apply
has always walked but which releases left empty.
Concave board features stop rendering inside-out. Reported in detail as #33 by
Sean Johnson, whose write-up pinned both sites, the convention, and the blast
radius — the fix below is his analysis applied.
stored as centre + start/end angle names *two* arcs, the counter-clockwise one
and the clockwise one, and both share their endpoints exactly — so normalising
the sweep is not angle hygiene, it *is* the choice of which arc to draw. Both
parsers had independently reached the same wrong idiom, reducing the sweep
into ±180° (a shortest-arc rule) and thereby swapping every arc over 180° for
the other side of its circle. Every notch, slot mouth and re-entrant corner
fillet became an outward lobe of the same radius, anchored at the same two
points. The sweep is now lifted into [0, 360) instead.
GenCAD had a second symptom from the same line: exporters write a full circle
as two endpoint-swapped 180° records, which the old clamp left at +π and
−π, so walking the second one backwards retraced the first half and the
other side was never drawn. 3,038 such circles in one reference file alone.
Nothing that renders correctly today can move: the two rules differ *only* on
arcs whose true sweep reaches 180°, verified across 19,964 XZZ arcs and 37,586
GenCAD arcs with zero geometry changes below that threshold.
PARSER_VERSION is bumped, so cached boards re-parse rather than serving the
old geometry.
The release signing key was unfortunately lost and had to be reset. This release
is signed with a new one, and adds the mechanism that stops the next key loss
from being a one-way door.
updater.PubKeys (new PUBKEYS build-arg, comma-separated) sits alongside the existing
updater.PubKey; TrustedKeys() merges them and VerifyManifestAny accepts
a manifest that verifies under any one. A key must still be compiled in via
-ldflags to be tried, so this widens the set of accepted *signers* and
nothing else — a trusted signer still cannot vouch for a tampered body, and an
empty key set fails closed rather than open.
outgoing key while the incoming key is already listed in PUBKEYS, and every
install rolls onto an image trusting the new key with no manual step. The
procedure is written up under "Rotating the signing key" in the release
runbook. Without it a rotation strands every install, because the fields that
would explain the problem — important_reason, notes_url — live inside the
signed manifest that the install is correctly refusing to read.
manifest that failed signature verification produced nothing in the UI at all:
the badge kept showing the installed version and the error went only to the
debug log, so a stranded install looked identical to an up-to-date one. The
badge now turns amber and the dropdown says an update exists, that this build
cannot verify it, and how to install it by hand — plus why the refusal is the
protection working rather than a fault.
Nothing from the unverified manifest is rendered. Its version, notes and
important_reason are exactly what a hostile mirror would choose, so only a
boolean crosses the trust boundary (signature_mismatch) and every word shown
is compiled into the build. A signature failure is also kept distinct from an
unreachable mirror (ErrSignatureMismatch), because those warrant different
advice.
Installs shipped before this release trust only the lost key and cannot be
reached automatically. They need one manual docker compose pull, after which
updates resume normally and this can never recur for them. The notice is on
https://www.ripperdoc.de/boardripper/.
No user data was exposed at any point. The key signed release manifests; it
granted no access to any installation, and BoardRipper stores nothing remotely.
Two boardviews that would not open now do. One is an .fz written with old-Mac
line endings; the other is a board delivered as a folder of loose .asc section
files, which from now on opens whole from a click on any one of them — and reads
two sections nothing had ever read. Plus the wedge that net highlighting cut
across boards with elongated pins.
.fz boardviews written with old-Mac line endings open. Some exportsend every record with a bare carriage return instead of a newline, and those
files failed with "contains no parts or pins" — the file was read and
unpacked correctly, but the whole board arrived as a single 745,000-character
line, so not one component was ever picked out of it. All three line-ending
conventions are now accepted. Canary: XPS 15 9530 Compal HD055 LA-L663P
Rev 1.0, which reads as 4,069 parts and 15,148 pins across 2,722 nets.
Reported by the user. f06769e2
.asc board opens whole from one click. The Tebo-ICT /eM-Test toolchain ships a board as a folder of section files, and opening
one gave a fragment: Pins.asc a board with no outline, Format.asc a bare
rectangle, Nails.asc an empty screen, and Nets.asc / Parts.asc an
error. Clicking any of them in the Library now finds the rest in the same
folder and opens them as one board, saying so in a toast. The OS file picker
and drag-drop cannot see the folder, so there a lone section opens as before
plus a toast whose "Add sections…" button takes the missing files. 6887a7d6
Parts.asc and Nets.asc are read. Two sections the obfuscated .bdvbundle never carried, so they had never been seen: the placement list, which
is the only source of part rotation and package name in the whole delivery,
and the net listing. Net names containing spaces (3D VISION) are no longer
truncated at the first word on pins and nails alike. Cached ASC boards
re-parse. 6887a7d6
with elongated pins — PL5TU1B, where 5,413 of 9,002 pins are capsules —
selecting a net cut a wedge over the board, and some connection lines ran
off to the board corner. Both were one defect: a capsule was drawn without
first lifting the pen, so it stayed joined to whatever shape had been drawn
before it, and to the origin when nothing had. Introduced in v0.35.0, when
pins started drawing as capsules outside the pad overlay. 27d17f00
A hotfix for one long-standing defect: XZZ board outlines came apart at every
rounded corner, and because the board fill closes each loose piece with a
straight line, boards showed black wedges cut across them. It dates back to
the first version of the two-halves fold.
apart — the edge broke into loose pieces at every rounded corner, and
because the board fill closes each piece with a straight line, the result
was black wedges cut across the board. The cause was a duplicate-edge guard
in the two-halves fold that compared endpoints with a fixed 1-mil tolerance:
a rounded corner is drawn as a run of 0.8-mil steps, and at that size a
segment is indistinguishable from the one it is joined to, so each corner
was deleted as a "duplicate" of itself. On the 16" A2485 the outline went
from one closed 767-point loop to 18 open fragments. Across the sample
corpus, boards with a correct outline go from 23 of 32 to 31 of 32 — the
remaining one has a real 26 µm gap in the file itself. Reported by the user
as long-standing; it dates back to the first version of the fold.
Connector legs on XZZ boards now draw with the hole in them — and where the pad
is an elongated stroke, with the slot it is actually routed as. Two formats
that had been sitting in the backlog are in: boardviews delivered as loose
.asc section files, and ASRock .cae. Plus release notes that survive the
update that delivered them.
Most of this release came from outside: Sean Johnson reverse-engineered the
XZZ drill field and the oblong-pad geometry down to the hex, and
@luiztessadri filed both format requests. Apologies to the latter for the
three-week wait — repair volume got ahead of the issue tracker.
to be gated on there being an update *available*, so applying one made them
vanish — leaving "You are on the latest version." and nothing to read, to
the one person who had just earned a reason to care. The notes for the
version you are running now live in the badge dropdown and on the start
page's Latest update card, cached locally at apply time so they survive a
post-swap network outage. fa7ad0f5
writes global, persisted settings — that is the feature — but a drag left no
trace, so a stray one read as a rendering bug. (It cost an afternoon chasing
"BRD pins are too big" that was pinSizeScale all along.) A row whose value
differs from the default now says so: an accent dot, an accent readout, and
its own reset button. The footer says where the change lands and puts the
whole group back in one click. 2e72faef
.asc files now open as one board. Somevendor tools ship the Honhan / Tebo-ICT boardview split into the three files
it is made of — outline, test nails, part pins — instead of the single
encoded .bdv. Select them together and you get one board, not three tabs;
open just one and you get what it holds (a lone pins file still gives you
parts, pins and nets). Requested by
#26.
.cae files are accepted. They are the same container and cipher as ASUS .fz and differ only in the key, so they now share the parser,
which tries each key you have stored and takes whichever one actually
decrypts the file. As with the FZ key, BoardRipper does not ship the ASRock
key — there is a paste field for it. Untested against a real .cae: nobody
has sent one yet. Requested by
#25.
.pcb files writethe BOM value into a label block on the part — "22uF" under C757 — which
the parser was skipping. It now reads it, so the Info pane, the net branch
list and MCP part search show a value where the file has one. Apple boards
are unaffected: their exporter fills the same field with placeholder text
(Device1, Device2, … one per part), and a board-level guard drops a value
column that never repeats rather than labelling every part with scaffolding.
Found, documented down to the hex, and prototyped by **Sean Johnson
(@sjohnson1021)** in
#27. 721a34b9
written off as a constant zero is the drill diameter, so connector legs,
headers and mounting pins now draw with a hole in them and their parts are
typed *through-hole* instead of claiming to be surface-mount. Where the pad
is an elongated stroke the hole is drawn as a matching slot, the way it
is actually routed, instead of a round dot floating in a blob. 5ca997e3
whether an elongated pad is real assumed the narrow side was always the same
axis. On a pad that ran the other way it flattened a 37 × 10 mil capsule
into a Ø37 circle — 3.5× too wide on exactly the connector footprints you
zoom in to read. Both axes are treated alike now, and the pads that really
are exporter noise (a "pad" one mil wide) are still filtered out. 5ca997e3
correctly-parsed oblong pads were still drawn as dots, and selection,
hover, net highlight, search and the HDR outline each stamped a circle in
the middle of one. All of them now trace the real shape, and the pin-size
slider still moves it. 5ca997e3
by Sean Johnson (@sjohnson1021) in
#32 — the kind of
report that arrives with its own hex dumps and worked examples.
On an HDR display the selected component's outline can now burn brighter than
white, so the part you are working on cannot be confused with the thousands of
white neighbours around it. Off by default, experimental, and invisible on an
SDR screen. Plus testpoint labels that no longer print on top of each other,
and an MCP auth switch that stops kicking agents out.
shape the selection already strokes (rotated OBB, AABB fallback, a 24-gon
around single-pin parts), redrawn as HDR line segments that sit exactly on
the SDR ones. Thickness tracks the normal selection stroke, so with the
feature off nothing about selection changes at all. 4af3e4ad e774ca67
intensity slider inline, because the right brightness depends on your
display and a trip to Settings discourages finding it by eye. The Settings ▸
Selection & Highlight control stays visible-but-hinted on an SDR display; the
start-page card hides entirely. 6cbbc9c0 e774ca67
(dynamic-range: high) is aboolean — a marginal HDR panel reports exactly what an XDR panel reports. So
the prompt shows a real HDR patch beside reference white and says outright
that if they look the same, turning the feature on will change nothing.
Dismissable for good. b10eb952
Rotate, mirror and butterfly all work for free — the layer projects through
the same matrix as the board labels. e774ca67
a PQ sprite keeps its headroom over the board canvas, but compositing it with
opacity flattens it straight back to SDR. So the intensity slider swaps
between 24 tiles baked from 4000 down to 200 nits (~7 KB in total) rather
than fading one. The finding, and the alternatives it ruled out, are written
down so they don't get "fixed" back into a bug. 14fbef4f 7c163591
594b997a
single-pin part — testpoint, fiducial, standalone pad — the two labels were
emitted at byte-identical positions and drew straight through each other.
They now straddle the pad the way a BGA's pin-number/net pair does, one above
and one below, sized off the pad instead of a zero-width box. eaf10d1e
0dcd63eb
Integrations back to Token used to kill live OAuth sessions instantly — the
tokens stayed valid, the server just stopped looking at them, while discovery
403'd so they couldn't reconnect either. The mode flag now means what it
always should have: whether *new* agents can onboard via OAuth. Ending a
session is an explicit act instead of a side effect. 699d453d
wrote it, and where to find the source — in a place you actually walk past.
It renders in every build, including lite and offline, which are exactly the
ones a first-time user is likely to meet. The Settings ▸ About tab that
briefly existed is gone again; one surface, not two. e5692c70 56a22dbc
e4930150
Click a pin and the Info tab now shows the whole net hanging off it — every
component that touches it, each one a click away — with the board pointing at
whichever you are looking at. Plus a much faster cold library load and two
rendering fixes.
other component sitting on the same net, each with the pins that land on it.
The first three are open and the rest are one click away, so a big rail never
buries the pinout underneath. 50d2519a 671de89e
neighbour's own pinout in place without moving anything; a single click flies
the board to it while the panel keeps your place; a double click makes it the
subject, with the net still fanned out beneath it and the part you came from
now a row in the list. Walking a rail is a sequence of steps you can walk
back. 50d2519a
five seconds on arrival — while that runs, disco mode goes quiet so exactly
one component is blinking — then settles into a slow flash every two seconds
that does not stop, so it is still findable when you look back at the board a
minute later. 0ade4c2a
only lit the net on the board while its components stayed out of view.
6a942658
the counts and the previewed row all derive from Settings ▸ Board ▸ Net Lines
▸ Color, so the trail in the panel and the lit net on the board are visibly
the same thing. Change the colour and the whole thing follows. 50d2519a
and answers nothing, so ground rails get no list. Which nets count as ground
comes from your own Ground pin group, so an unusual name is taught once in
Settings. 671de89e
count now live on the pin row itself rather than a bar of their own.
6a942658 bb31d55c
database never reclaimed space: splitting out the PDF text tables in v0.31 had
freed 1.45 GB to its free-list, but nothing ever compacted the file, so it
stayed at 1.5 GB with 96% of its pages empty and every cold load paged the
file list across all of it — around 29 seconds on NAS disk. It is compacted
now, once, on upgrade. 8b66cc8b
more boards open, each board's panel had mounted its own copy of the popup,
and every copy treated a press in the visible one as a click outside itself
and closed the shared popup. One board was fine; two made Interactive Mode
unusable. c375c57e
.cadpin had used a fixed 6-mil radius, which is right only for exports that
declare their units; on files that do not, pins rendered smaller than their
own pin-number labels. The radius now comes from the pad geometry in the
file's $PADS section. 5b50bc76
end-to-end and 19-pixel rows land on fractional device pixels, so opening a
component's details left hairline gaps in the line. 0ade4c2a
Altium Designer files now open — the twelfth supported format. Plus the Tools
tab (component-code calculators and a worklist catalog), a much faster Library
on slow connections, and a container-build fix that means two earlier fixes
actually reach you for the first time.
.PcbDoc files open directly — Altium Designer 6.0 and later, plus the Circuit Maker (.CMPcbDoc) and Circuit Studio (.CSPcbDoc) variants, and
the PCB ASCII v5.0 text export. Components, pins, nets, tracks, vias, arcs,
fills and multi-layer copper all come through; every pin carries its net, so
net highlighting and connectivity work as they do on any other format.
shape varies per layer (common on BGA stackups) are not sized yet and render
small — on our 4-layer test board that is 40 of 100 pads. Inner copper layers
are parsed but only the outer two are listed, and the board outline is a
bounding box rather than the true shape. Connectivity is unaffected. Expect
a net list you can trust and a picture that is not yet finished.
toggle in the sidebar controls them like any other format's.
including the letter-position rule that tells EIA-96 from R-notation apart.
instead of only the one attached to the open board.
the complete file list before drawing anything; it now renders as results
stream in, and opening a file no longer waits for the whole list to load.
5b452db7
the wire — a large collection drops from tens of megabytes to a few, so a cold
load over a slow connection finishes in seconds instead of most of a minute. A
new "Bulk API" load mode in Settings ▸ Library offers a single compressed
download as an alternative to streaming, and streaming falls back to it
automatically if it fails. 663142d4 91d73e98
stopped, can be restarted from Settings, and picks itself back up
automatically after a container update. d5acd945
Both were in the source (since v0.30.3 and v0.31.36) but were being dropped
during the container build, so no web or NAS install has ever run them. If
watermark filtering looks new to you, this is why.
the pan that followed it.
pads when you flip top/bottom, mirror, or change the flip axis, not only on
rotation as before. efb5bb03
selecting from the dropdown blurs the field (so Space flips the board again
instead of typing a space) while keeping your typed text; re-focusing reopens
the list with the active net/part clearly marked. efb5bb03 fbd19212
and because the container build was chained behind that lint step, the
container build and smoke test were being skipped entirely rather than run.
Both now run independently.
Interactive Mode: size any board element by clicking it. Plus the offline
single-file build, and a batch of rendering fixes — including two faults that
could stop the renderer mid-session.
Board turns a plain click into a sizing gesture: whatever you click opens a
popup at the cursor with the handles relevant to it, and the whole board
previews live as you drag. Pan and zoom keep working. f37157a8 1c163d36
size, net-label size, diode-value size, selection-highlight size/opacity and
the selected-label floor/LOD; a component offers its label size and outline
width; a highlighted net's connection line offers width, colour and opacity;
empty board offers board opacity. Every handle carries a one-line
description, scroll-to-nudge and double-click-to-reset.
c798d216 a387fb20 746e761a 82070a22 e83eed9a
component-label and diode-value sizes are now true multipliers, so they
scale on every board instead of only nudging labels that happened to sit at
the old minimum-size floor. 6bee409f 9f80f81e b40523b4
(darklight) dimming and a part selected, the selection halo was destroyed
along with the scene on each rebuild and then drawn again, halting the
ticker until "Restart Render". 856c4343 5ff71f69
geometry is chunked below the 65,535-vertex index ceiling, and the chunk
budget now adapts when pins are scaled up.
657ea5ef c40dffde 3df9f6f0 d656e3c9
to a ghost as it grows, so the net names underneath stay readable instead of
being blanketed by it. 1ab634f0 8babb0cc
label and selected the whole component, which highlights no net; clicks now
resolve to the specific pin. e1805871
6a1777cf d7b0ed8e
boardripper-lite.html that runs straight from file://, downloadable from the toolbar, with the lite web app
deployed alongside each release.
2f39348b a23175ac 9aaa692d 5dc6d080 954f614b
Folders source menu. 263686e1
claims its slot immediately instead of after a long enumeration, so extra
clicks fail fast rather than queueing a "stop previous index?" dialog each.
b2ca9c82
collapse when there isn't; the search bar moved below them.
077b6827 6c74fe14 686d6439
6e8c8f56
parsed data lands. 2b3987b0
MCP auth follow-ups to v0.31.42's session separation: clearer errors for
logged-out agents, and mixed authentication so OAuth users and token users
can coexist on one install.
mode: per-browser and shared bearer tokens keep working when
Authentication is set to "OAuth (no token)", so some users can connect
via OAuth while others keep their tokens on the same install. 16e0b674
carry an RFC 6750 WWW-Authenticate challenge explaining the v0.31.42
token reset, and the OAuth endpoints answer an explicit 403 ("OAuth is
disabled on this install") in token mode rather than a hidden 404. The
404 remains only while the MCP server is disabled entirely.
b35c1c7c 16e0b674
Multi-user MCP: per-browser agent pairing. On installs shared by several
technicians every agent used one shared token and targeted the
most-recently-focused page install-wide — so one user's agent could read
(and even drive) another user's board. Sessions are now separated by
pairing each browser with its own token.
token sources: "This browser's agent" (default; scoped to the boards
open in this browser, with an editable label and one-click rotate) and
"Shared (all sessions)" (the install-wide token, kept for
deliberately analyzing other users' sessions). An agent with a paired
token only sees and drives its own browser's pages, and its default
target is that browser's focused page — wrong-board mixups between
users are gone by construction. e2c29dde 4c1b1c90 84b91b63
f1dd68cd 95b1526c
shared MCP token is regenerated once: agents configured before the
update are logged out, because resetting the shared credential was the
only way to migrate it properly. The 401 response and a notice in
Settings ▸ Integrations explain the reset and point to the new tokens;
open BoardRipper pages re-join the bridge automatically. fec25f66
client_label and focused_at_ms, so multiple open sessions are distinguishable — and
with the shared token lists every page, while a paired token lists only
its own. f1dd68cd
steals the "most recently focused" default target from the window the
user is actually working in. 84b91b63
Renderer follow-up round: less per-frame work during interaction, no more
rebuild storms across tabs, and Text fast mode is now on for everyone.
the default for new installs in v0.31.40). A one-time notice on first
start explains the change and links to Settings; the previous text
renderer remains available under Settings ▸ Performance & Debug ▸
"Text fast mode", and turning it off sticks.
labels, matching the classic renderer.
stale transforms.
with a plausibility guard against implausibly huge pins (PL5TU1B).
7a474e6a *(shipped in this release but missing from its original
notes; added retroactively.)*
inactive tabs rebuild once, when you switch back to them.
entirely (previously re-stroked every frame while the viewport moved).
rebuilding all line geometry 60× per second.
changes, not on every hover while dimmed.
after fold/filter toggles; deep-paused background tabs now release the
fast-mode text layer's memory too.
Merges the renderer-optimization branch — the implementation round of our
renderer performance plans from earlier this summer (see
docs/research/rendering-review-2026-07-12.md and
docs/research/wasm-webgpu-acceleration-plan.md).
overlay instead of tens of thousands of in-scene text objects — much
higher FPS on label-dense boards (11→60 fps measured on a dense board).
The previous text renderer stays available: Settings ▸ Performance &
Debug ▸ "Text fast mode".
the cursor at the same speed as before. Disable in Settings ▸
Performance & Debug. Trackpad pinch, Ctrl-zoom and drag-zoom unchanged.
"Selected Part Labels" slider (Settings ▸ Zoom Level of Detail) controls
their minimum on-screen size.
zoomed in — long-standing culling bug, affects both text modes.
frame, tooltip measurement cached, trace lookup on a spatial grid.
A small fix round: converter-produced GenCAD files open again, XZZ diode maps
draw their zero readings, and pin numbers can be toggled straight from the
View tab.
exports (e.g. DAX3ACMBAF0 X3AC Rev F.CAD) start with NUL bytes before
$HEADER, which content detection didn't strip — so the .cad fallback
routed the file to the Mentor Neutral parser, which rejected it with
"contains no placed components". Detection now tolerates the NUL prefix;
the X3AC sample opens with 2,834 components / 1,732 nets. (7c00bd90)
=0= records in XZZ diode maps wereclassified as "not measured" and hidden from the on-pin overlay, tooltip
and component info — but a literal 0 is a real measurement (short to
ground), and on connector diode maps it's most of the table (776 of 1,144
on the iPhone 15 Pro map). Zeros now draw as "0" on the pin and show as
0.000 V in the tooltip and info pane; cached boards re-parse
automatically. (9d178fa5)
labels compete with the on-pin readings; hiding them previously meant a
trip to Settings. The View tab now has a Pin numbers visibility toggle next
to Diode values, mirroring the same global setting. (ec30722c)
Also riding along: the desktop MCP-sidecar groundwork from v0.31.38 (an
Electron-only release) is in a Docker image for the first time here; it is
gated on Electron and inert in the web build.
An MCP release: the assistant can now drive the live board — open library
files into tabs, work across several open boards at once, and read a
selected component's pins — plus a smarter library search that finds a board
by any of its metadata, including its folder path.
open_file tool loads aboard (and its auto-bound schematic PDFs) or a document by file id, so the
assistant can go from a search hit to an open board on its own instead of
waiting for the user to open it. (9eb9f0f6)
board_tabs lists the open board tabs and switch_tab changes the active one; the inspection tools
(part_info, net_info, list_parts, …) take an optional tab, so the
assistant can compare two boards without losing its place — e.g. a
pin-for-pin comparison of the same chip on two variants. (439f921e,
9eb9f0f6)
selected_part returns the pins andnets of whatever the user has clicked, so the assistant can pick the
conversation up from the board. (439f921e)
file_list now matchesthe folder path, manufacturer and extension in addition to filename /
board number / model, with multi-word AND matching — so searching an Apple
A-number like "A2991" returns its boardview even though the file is named
by its 820-number and sits in an A2991 … folder. (1d05729a)
assistant that boardview files (BRD/FZ/XZZ/BDV-ASC …) are encrypted or
obfuscated and must be read through the live board tools, not fetched as
raw bytes. (439f921e)
board_sessions. It failed output-schema validation and returnedan error instead of the list of open boards; now returns cleanly.
(1d41e169)
board by any metadata including its folder path, with multi-word matching,
so typing an Apple A-number surfaces the boardview even when its filename
only carries the board number. (23ad492f)
A memory and parse-performance release: closing board tabs now actually
returns their memory, big boards parse without freezing the UI, and the
status bar shows live memory usage.
every opened board stayed in memory forever — its parsed data (~15 MB per
board, held via leftover viewport/resize callbacks) and its entire GPU
scene (batched geometry and buffers, ~130 MB per few boards, pinned by
PixiJS context-loss listeners and a pooling bug patched via
patch-package). A session that opened and closed a handful of large
boards used to park at 1 GB+; the same cycle now settles back near
baseline, and background-tab deep-pause cycles no longer leak either.
A regression test with GC-forced WeakRef canaries guards both fixes.
(d5e16ee9, 2f3e49a6)
(including the parse worker) where the browser supports it — the app is
now served cross-origin isolated to unlock that API — and falls back to
an approximate heap figure marked ≈ elsewhere. (466e2b74)
(100+ MB Allegro/BRD/TVW) no longer freezes the tab for the whole parse —
the UI stays responsive while the progress overlay runs. Encrypted FZ
files, the key dialog, and drag-and-drop all behave as before; if the
worker is unavailable the old inline path is used automatically.
(1e6d7e1a)
IndexedDB write (hundreds of ms on big boards) now happens in the
background, and a failed write no longer fails the whole load.
(2f1a80a8)
dropped its all-pairs scan for a spatial hash, ghost-component detection
got a sweep prefilter, and the FZ RC6 loop lost an array shuffle — each
parity-tested byte-for-byte against the previous implementation, so no
re-parse of cached boards is needed. Parse stages now log timings to the
Debug panel (perf scope). (ce455ab1, d7af8e5e, 2fb7723e,
3986f879)
pad geometry from the parser shrank them. (7b14c221)
A selection and small-UI fix pass.
a zoomed-out view the "pin click radius" (a screen-pixel grab distance) grew
large in board units, so a small surrounding test point registered as the click
and — being the smallest thing there — won selection. Clicking inside a
component now takes priority over a merely-nearby pin, and the default Pin Click
Radius is tighter (30→15px). Genuinely stacked components still cycle
smallest-first. (eb5eac27, ec18e86c, #24)
mirrored.** The white highlight copy of a selected / net-highlighted name stayed
frozen at its old orientation while the base name re-oriented, leaving two
overlapping offset copies; the highlight now re-syncs on rotate/mirror.
(eb5eac27)
into one very wide line with its own line breaks collapsed; it now wraps and
keeps its newlines. (d9127ee2)
(not HTTPS or localhost) the browser Clipboard API is unavailable, so the button
silently did nothing; it now falls back to a copy path that works there.
(d9127ee2)
Continues the MCP build-out: a connected AI assistant now arrives primed as a
repair technician and can pull on a built-in repair knowledge base. Off by
default; enable it in Settings ▸ Integrations.
connection now carries a persona: understand the circuit before judging it,
work from evidence not guesses, treat unlabeled nets as low-trust, follow
sound measurement practice (diode mode for data lines, not power rails; ohms
and diode only on an unpowered board; continuity mode for continuity only),
and teach the *why* in plain terms — calibrated to how much guidance you want.
(d59e812)
understand_circuit, diagnose, and explain (they appear as slash commands in Claude Code) walk
the assistant through learning a circuit, running a symptom-driven diagnosis
on the shared worklist, or teaching you about a net/part/concept — each
grounded in the board you have open. (093a5c4)
and concept notes on demand — short-to-ground localization, measurement
hygiene, when diode mode helps vs misleads, power-rail basics — through a
kb_search tool and read each note as an addressable resource. It searches by
relevance rather than loading everything, so the connection stays lean.
(a779430, 4800d3c, 53719d3)
Expands the MCP integration so a connected AI assistant can actually see and
read the board it's helping with — the first of a phased build-out. Off by
default; enable it in Settings ▸ Integrations.
board_snapshot captures the live board view as an image and pdf_page_image renders any page
of the open schematic, so a copilot can visually correlate a part on the board
with its symbol on the print instead of working from net names alone.
(32ccf34, 6789416)
pdf_page_text returns a page's text and pdf_search_open searches within the open document
instantly; the existing library-wide pdf_search now also accepts a single
file id to scope a search. (f5db6d4, 908da01)
pdf_download hands the open schematic's bytes to the assistant and file_download retrieves any indexed
library file by id, so a search hit becomes a document the model can read
end-to-end. (4730d06, 3c49c0e)
board_overview reports the open board, itspart/net counts, shown side, every open PDF (name/page/id), and a worklist
summary in a single call — and the bridge now tells the assistant which PDF
you have open. (39f30a1)
tagged named or synthetic, so it won't read a function into an
auto-generated name like N$123. (cba1b66)
part, or flipping sides now returns what actually happened (pins highlighted,
part found and its side), so the assistant can confirm an action instead of
guessing. (4fc071c)
WebSocket capped messages at 32 KiB and tore down the connection on any real
image or PDF; the limit is raised so snapshots, page renders, and downloads
round-trip cleanly. (a4ef066)
A memory-optimization pass. Across a long session BoardRipper now releases
memory back to the system instead of holding on to many gigabytes — both in the
browser (open boards and PDFs) and in the Docker backend.
full WebGL context and scene graph alive for the whole session, so opening many
boards piled up gigabytes that came back only slowly. A board that stays hidden
now releases its renderer and rebuilds — restoring the exact view you left — the
moment you switch back to it. (1301612, 2935c2d, 6d1761d, 697ce08)
visited page's render data until the document was closed; page resources are now
trimmed as you navigate, off-screen tile and preview images are freed promptly
instead of waiting for the browser's garbage collector, and clean mode no longer
keeps a second copy of the document loaded once it's switched off. (31cc953,
84ea23a, 200b2de)
memory budget from its container limit and hands freed pages back promptly; the
PDF-index workers cap and recycle their memory instead of holding a permanent
high-water mark; large files stream to the browser instead of being buffered
whole; and a real container memory limit is now actually enforced. (c5260ff,
ad6d9d2)
textures and detaches its ticker on teardown, and per-board diagnosis lookups
are capped so a long browsing session doesn't grow them without bound.
(b6c104c)
A broad correctness-and-hardening pass from a full multi-directional audit, plus
a Library panel cleanup.
files that vanished from disk were never removed from the database after the
first restart, so the folder tree, file list, and duplicate groups slowly
filled with ghost entries. (1227506)
clears its old pages first, so search never surfaces text from a previous
version of a file, and the watermark re-index reliably strips added watermark
terms. (1227506)
(slow boot, transient Docker error) used to consume the release and could never
be re-applied from the in-app updater; it now re-offers the same release, and a
Docker image-load error is detected instead of being reported as success.
(1227506)
actual board and PDF files, closing an over-broad path that could reach other
files in the data directory. (1227506)
millimetres are scaled properly instead of collapsing to an invisible speck.
(409f570)
get a correctly-oriented outline box, and formats that can't distinguish SMD
from through-hole now report "unknown" rather than guessing "SMD". (409f570)
casing no longer create duplicate "(missing)" rows, an out-of-range repair mark
can't corrupt the board highlight, and measurement requests reach the right
net. (c5d4d3a)
resize and made in-document search noticeably faster on large schematics.
(7efd5b3)
Board# and Model views no longer stay empty while files stream in. (90ac02a)
file surfaces the "materialize on host" message immediately instead of retrying
for three minutes. (90ac02a)
fire while a list panel is focused, and bright accent colours stay legible as
text on light themes. (8357207)
status line is a single row pinned at the bottom that expands — on click, or
while a scan/index is running — to the full breakdown, keeping the "view failed"
button; and switching tabs focuses the search field so you can type straight
away. (d45d37c)
small Database / Live menu that auto-dismisses, instead of taking a permanent
row for one control. (8248438)
is bounded and hidden when the integration is off. Renderer pan/zoom hot paths
shed redundant per-frame work. (1227506, c5d4d3a)
Select components hidden under others, and get a clearer read on what's selected
versus what's merely on the same net.
shared pads, a small part on a big pad, a component under a shield), a click
now selects the smallest part under the cursor; clicking the same spot again
cycles through the stack. On 2-pin parts the second click selects the whole
component — the first still selects the pin/net. Right-clicking a stack lists
every overlapping part so any of them can be pinned to a worklist or looked up
directly. (be05bef, 0358378, 6ec512b)
white outline with a white name label — distinct from the net-member parts it
shares a net with (which stay in the net colour) and from the worklist mark
colours. The selected name stays white at every zoom. (6ec512b, aeeaced,
9142a7d)
on/off now updates the on-board outlines immediately instead of waiting for the
next pan or zoom. (aeeaced)
when you click the board or switch the sidebar tab away and back. (#22,
6961bb8)
per-part geometry (all-zero part corners, e.g. X540UV 60NB0HF0-MB1020) had
every part's outline and label pulled toward the board origin; parts now derive
their box from their pins. (e8aa6b7)
The session-restore boot prompt now lets you pick which boards and PDFs to reopen
instead of restoring all-or-nothing.
lists every board and PDF that was open with a per-file checkbox (all checked
by default). Uncheck any you don't want and only the rest reopen; the primary
button reflects the count ("Reopen (N)") and disables when nothing is selected.
Discard still clears the whole saved session. (e4ca016)
Board↔PDF links now persist across reloads, the worklist records full multimeter
readings per net, two reported bugs (#20, #21) are fixed, and the tab/tooltip
chrome got a round of polish.
∞ nowwrites a durable backend binding — a single row no matter which side you link
from — so the link survives a reload + reopen, and auto-open / auto-switch keep
working. Unlinking *demotes* the link (it won't auto-open or get resurrected)
instead of destroying it. Dropped files are ingested into the library with a
real id at drop time, so they bind and restore just like library files.
(9b18933, bc2855a, 7ed1c8a)
a diode drop, *and* a resistance reading at once — three independent slots
instead of one value you had to switch the type of. Copying a worklist carries
the nets and their readings as round-trippable text. (c869aaa, 62ea78d,
8d06ceb)
the active worklist shows its mark + note inline; net readings render with the
diode glyph. (e459913, 69bee13)
preserved image/env/mounts/ports; it now also re-attaches the container's
user-defined networks (with aliases), Compose labels, and memory/CPU limits —
fixing the reverse-proxy 502 that hit networked deployments after updating.
(cd95a7d)
no longer snaps the view to the top-left. (8b8dc67)
PDFs invisibly (no panel); they now reopen, and the "reopened N" toast counts
what actually opened. (f2cf83c)
∞ / ○○ glyph is now proper link /link-off icons (identical size on Chrome and Safari). Long file names in tabs
are truncated with a hover tooltip. Tab colours no longer flip when a group
loses focus, so board↔PDF switching is visually stable. (bc2855a, f2cf83c,
af1731b, 0f7609d)
and the PDF ∞ "auto-open boardview" toggle is renamed **"auto switch
boardview"**. (f2cf83c, bc2855a)
Reload no longer loses your work, and the update dropdown now shows what's new
before you update.
boards/PDFs were open and the page reloads (manually or after a self-update),
BoardRipper now asks whether to Reopen or Discard the previous session.
It never auto-restores — so if you reloaded *because* something hung, you just
discard and nothing reloads. The open set is persisted continuously (so even a
hard hang/crash leaves a record). On Reopen, library files re-fetch from the
server, dropped files come back via the library's incoming/ folder, and a
dropped board that isn't on the server falls back to the local parsed-board
cache; anything genuinely unavailable is reported. (188e3c6, 28d9b13,
c7d7802, b2fbb48)
release's notes inline, under a collapsible What's new spoiler, instead of
only linking out. The notes are embedded in the signed update manifest
(sliced from this changelog at release time), so they're trustworthy and work
offline; releases without embedded notes fall back to the "Release notes ↗"
link. (Appears for updates *to* releases cut from here on.) (94108dc,
e7d88c0, 65fb86d, 5afddcc)
Two new features — a Donor boards library tab that turns marked PDFs into a
managed, auto-indexed, reset-proof donor pool, and Worklist v2 Phase 1
(inline per-net measurements + a relay-only AI section) — plus interface and MCP
fixes.
row showing its live index status and linked board. Click a row to open its
detail (bindings + Open); double-click to open the PDF. Export / Import /
Restore controls manage the pool. (e562176, 212e08e)
triggers a scoped backend pdfium index of exactly that file (server-side, so the
UI, an MCP tool, or a script all benefit), plus a boot-time backfill of any
donors that weren't indexed yet — so "search donors only" no longer silently
misses a never-opened PDF. (b4367c4, af499d2, e148eae)
auto-written before a reset and restorable in one click, with manual
Export / Import for portability — wiping the scan no longer loses your donors;
they re-resolve to the rescanned files by path. (98308fd, e9c8cc5, 0df3351)
V / Diode / Ω strip where the technician types a reading directly — no separate
"to measure" list. Values are stored as NetMeasurement objects with unit,
value, timestamp, and source (user | agent). (a288871, 4a73959)
transcript (messages + prompt box) only — measurement requests that target a
net route to the net row's inline field, while part/pin targets land in the
relay as a pending row the user answers or skips. (1b7902c, 7429650)
get_measurements. The MCP get_measurements tool returnsall net readings regardless of who recorded them — the technician typing a value
directly on a net row and the agent requesting it via request_measurement both
appear in the same snapshot. Optional status (pending | answered | skipped)
and source (user | agent) filters narrow the result. (7429650)
old always-on automatic outlines: when on, worklist parts render in their mark
colours and any net shared by two or more worklist parts glows; when off, the
board is unaffected. The Clear button is decoupled from the toggle. (b200bb6,
85a00f9)
Phase 1 migrates any flat-array AI measurement entries onto the corresponding net
rows automatically, so existing worklists carry forward without data loss.
(7f2e5cc)
pure-CSS pseudo-elements that couldn't clamp to the viewport, so buttons near
the right edge ran off the screen. They're now a single JS-positioned tooltip
that clamps horizontally and flips above the button when it would overflow the
bottom; the board hover tooltip also clamps on every edge. (8e97b36, be1c442)
sidebar (width persisted to localStorage), its tab labels no longer sit under the
☰ toggle, and the redundant × close button was removed — the ☰ toggle is the
single close affordance. (be1c442, 8e97b36)
means) now tears the WebSocket bridge down instead of reconnecting to a dead
endpoint every three seconds forever; each reconnect re-checks that MCP is still
enabled before retrying. (526f0a7)
Smarter schematic lookup, and two fixes that make ASUS laptop .cad
boardviews open correctly.
9b6996b, 2bc2cb4, 84a1548).cad boards no longer open as a pile of giant overlapping components. TESTCAD/IMPACT exports (FA506QR, X415JA, G513IM…) store each part's true position in its shape coordinates and use PLACE only as a tiny nudge — the inverse of the case the shape-recentering logic was built for. Recentering crushed the board to a fraction of its size and left components 5–50× oversized (one part covering half the board). BoardRipper now detects this convention file-wide and skips recentering, restoring the real layout — cross-checked against the FZ export of the same chassis. Clean GenCAD, multi-revision V382, and Quanta files are untouched. (6310b1a).cad boards that froze the tab on open now load. The same exports re-list each component once per net/device record, so a large BGA listed thousands of times over a full-footprint shape produced millions of phantom pins and exhausted memory (FA506QR ≈ 9.1M pins). Consecutive byte-identical component records are now collapsed to one; genuine multi-revision files are unaffected. (7e878aa)Extends the MCP integration with a two-way worklist AI-mode loop — an
agent (e.g. Claude Code via the BoardRipper MCP) and the bench technician
collaborate on the same worklist, no in-app LLM required.
worklist_get / worklist_get_measurements / worklist_get_user_messages (read) and worklist_add / worklist_update / worklist_set_list_note / request_measurement / post_message (write, gated on the mcp_drive_ui toggle) — plus matching bridge dispatch handlers. The WorklistPanel grows an AI section: measurement rows with answer/skip, a relay transcript with a prompt box, and clickable [n:] / [p:] net/part chips (the DiagnosisNotes note renderer is now exported and reused). The boardripper-repair-helper skill gains a guided worklist-loop playbook and tool quick-reference. Design spec: docs/specs/2026-06-16-worklist-ai-mode-feedback-loop-design.md. (a2d21b3, f77eb38)BoardRipper can now be driven by an AI agent: a built-in MCP server exposes the
open board — its connectivity, the schematic PDFs, OpenBoardData and the board
database — to Claude Code (or any MCP client), and a bundled repair-helper skill
turns that into a hands-on bench copilot.
/api/mcp), off by default, enabled in a new Settings ▸ Integrations tab. 21 tools: PDF full-text search, OpenBoardData diode/voltage/resistance readings, board-reference resolve, and file inventory answered by the backend; plus — over a WebSocket bridge to the board open in your browser — live connectivity (list/inspect nets and parts, pin-to-pin, net-neighbour power-sequence tracing) and drive-UI (highlight a net, select a part, flip the side, jump a linked PDF) so the agent narrates *and* shows the answer on your screen. (74dfdb4, 6ea651c, e742cdf)part_info returns a component's full metadata — value, serial, package, type, side — and a new find_parts free-text-searches reference designators *and* those description fields, so a component can be located by its real name/number even when no schematic PDF is available. list_parts/list_nets gained substring/side filters + pagination, and board_active reports a generation token so the helper notices when you switch boards. (b936dc0, bf45adc)boardripper-repair-helper) that turns the raw tools into a structured copilot with playbooks for find-a-component-by-function (evidence order PDF → part descriptions → OpenBoardData → connectivity), power-rail / no-power triage, signal tracing, and short-to-ground hunts. (b936dc0, bf45adc)e9e8556, ed9ff71, 5de9a60)A theming overhaul: three light themes, an editable custom theme, and a
net-class pin-colour system — all carried by the theme — plus the cleanup
that makes light themes genuinely readable.
5273252)0f80877, d8878f1)* and # wildcards). Analog ground gets its own shade (AGND ≡ VSS), the Misc group is "outline-only" for no-connects, and the whole palette is carried by the theme. Component fill colours and the per-side default pin colour ride along too; the Pad/Body shape columns were retired from the editor. (1052c3b, d8878f1)c1be911, 3c89432)9e3cea6, 6c22b0b)c269ffd)c35f666)setActiveBoard only recomputed for per-board overrides, not theme ones. (6c22b0b)6883f17)Two features: PDF orientation controls and a diode-value channel for XZZ
boards.
fe6d9f4, 2de7955, 8bbed00).pcb companions carry reference diode-mode readings in a table after the file's v6 marker; these are now parsed and drawn directly on the pins (white, per-pin), with OpenBoardData as a second source feeding the same display via each pin's net. Toggle it from the board sidebar's View tab (or Settings ▸ Pins); readings also appear in the hover tooltip and the Component Info pin table. The control only shows on boards that actually carry readings. (3d4b6b6, e017f3d, 1bda8a9, fa9513c)Point fix for the board-side link menu shipped in v0.31.19.
position:fixed resolved against the transformed ancestor instead of the viewport, landing the menu in the wrong place. It now renders through a portal to <body> with measured viewport coordinates, so it drops down directly under the icon; outside-click detection checks the portaled node, and it holds off first paint until positioned. Added a geometry regression test (asserts the dropdown box is on-screen and anchored under its button — verified failing on the pre-fix code) since the original toBeVisible() probe passed even with the menu off-screen. (c7bcdc8)A broad UX pass driven by a six-phase improvement plan plus bench
feedback, layered on top of the library drop-to-incoming and
auto-indexing work. Headline: the in-tab board panel now opens on
component Info for single-layer boards instead of the visibility
tab, board↔PDF linking works from both sides, PDF paging and search
stop losing your place, Settings commit behaviour is consistent, and a
clutch of long-standing rough edges (the vv0.31.x version badge, the
interface-scale slider jitter, the inconsistent library stats line) are
gone.
incoming/{brand}/{model}/ (with a brand fall-through when the model is unknown), the new row is force-streamed so it appears immediately, and dropped PDFs are auto-indexed for full-text search. The DB folder tree gains a per-folder index button. (335dec1, 311a08f, 49f2a8e)d7e014d)325afda)0/0 with a one-click handoff to the library-wide PDF search. (0495267, 42a34f9)? anywhere to bring the full keyboard-shortcut cheat sheet up over your work — previously it was only visible on the empty home screen. Tab-jump and arrow match-navigation are now listed, and ⌘O/⌘P collapse to one "Open File" row (they open the same picker). (42a34f9)0495267, db72f69)567366a)1f66453, 7a5268f, 3e57b38)98de9d5, bb4d2e1)30c1d9a)f0bf6d7)8e64479, 48c672f, bc49a0b, f0bf6d7)vv0.31.x. The backend version already carries a v prefix, so the up-to-date badge double-prefixed it while the update-available path showed it raw — the two never matched. A single normalizer fixes all five display sites (and the dev-build "vdev"). (325afda)bb4d2e1)bb4d2e1)db72f69)resolution_status === 'resolved'), and the re-resolve gate triggers on any metadata-field change. (70433d7, 0fa860d, 7005f75, 73aec8c)Bigger update than the recent point-releases — covers four areas. The
library-list pipeline was rebuilt around a streaming endpoint with an
IDB chunk cache so opening a 100 k-file library no longer freezes the
UI; the PDF text-click path got a stack of lookup-precision fixes;
holding Space now lets you peek at the other side of the board and
returns on release; the Landrex theme's selection halo behaviour
matches its lower-contrast aesthetic.
res.json() on /api/databank/files parsed the full 50–100 MB payload synchronously, the search input was frozen until the parse finished, and there was no progress UI. New backend endpoint GET /api/databank/files/stream emits NDJSON (begin → one file per line → done), the frontend reads via fetch().body.getReader() and batches 2 048 rows at a time, yields the main thread between batches, and surfaces a progress strip + counter inside the Library statsbar. Filter input gets a "streaming database from server… 12 k / 80 k" chip so partial-state search is discoverable. ETag/304 fast-path preserved on both endpoints. (0a61705, 9ca16b9)ff36fbe, 47d239f, e1f3549, 5ad47e7, da114ee)b291ad5, f8ea9b4)cursorReq.onsuccess = async () => { await onChunk(...); cursor.continue(); }. The await created a microtask gap where IDB had no pending request, so the read transaction auto-committed and the cursor silently aborted. tx.oncomplete then fired "successfully" with only the first chunk — every warm load stopped at exactly 2 048 files regardless of how many were cached. Made the cursor handler strictly synchronous (no async, no await, no Promise<void> return — and the doc-comment loudly forbids reintroducing them). Added a sticky "Library load incomplete — N of M files" chip with a one-click Reload that bypasses the cache via fetchFiles({ force: true }) so a torn cache always recovers. (51ffbfa, 189acfd, 07fc4eb)fetchFiles() coalesces against _filesInflight, so the post-scan call was awaiting the pre-scan promise and returning — the post-scan file set never landed. And _doFetchFiles's in-memory shortcut could collide on a signature-stable rescan, skip libraryLoadStore.begin/setPhase entirely, and leave the progress strip permanently hidden. Drain the inflight load and null out _filesComplete/_filesSignature before the refresh fetch in both _startScanPolling and stopScan. (7a8db93)extractWord keeps decimal values whole. Clicking on 0.001Ohm or 0.002 used to split into 0 and 001Ohm / 002 because . was a hard word boundary. . now counts as a word char only when it's a true decimal point inside a number (digits on both sides, digit-run-tail not preceded by a letter/underscore) — so R5960.3 / U5960.6 still break at the dot for designator→pin lookups, but 0.001Ohm stays one selection. Click exactly on the dot of 0.001 also seeds the expansion. (33e8d20)extractWord keeps digit↔digit space so designator+pin lookups don't fuse. Adjacent runs like R5960 1 were fusing into R59601 for the click-extracted word because the consolidated text-item string lost the visual gap. Preserved the boundary so designator and pin number are separately lookable. (097bb7c)extractWord correctly returned R5960, but the rendered highlight rectangle was inflated by pad = 2 / z pixels on every side plus a 1.5 px border. In dense schematic layouts where the designator and its pin label sit a sub-pixel apart, those extra pixels visibly extended the orange highlight box over the next glyph — looking like the click selected R5960 1 together. Dropped the horizontal padding so the rect hugs the word's actual extent tight. (33604a6)112e773)manufacturer is empty but whose board_manufacturer (ODM) was pattern-matched now group under [ODM] Wistron / [ODM] Quanta / [ODM] Compal instead of all piling into one giant "Unknown" bucket. (189acfd)fetchTree still hit the network every time — ~700 ms over a typical home/Tailscale link plus JSON.parse on a 2 MB body. Tree now lives alongside the file chunks under the same last_file_scan_at:count signature so they invalidate together; the first Folders click writes the response back to IDB and every subsequent visit is instant. Folder-tree fetch also surfaces an indeterminate progress strip during the cold path (no per-row signal available — it's one HTTP call), and prefetches in the background after every fetchFiles so the first visit is usually already warm. (b7541c1, c992250)delete msg.type instead of { type, ...rest } per-row spread (≈ 100 k allocations saved per cold load on a big library), backend NDJSON encoder gets SetEscapeHTML(false), and WHERE file_type IN ('board','pdf') matches the streaming query to the ETag's count source so the begin.total advisory can never overshoot. (51d2493)Follow-up to v0.31.16. Same root cause (pdfjs-dist@5 targets a modern V8
baseline the Legacy Mac build's Electron 22 doesn't have), broader fix:
audited every pdfjs reference against Chromium 108 and polyfilled the
full set so this stops being a whack-a-mole.
Promise.try and PDFs would open — image sprites rendered, but no text. The worker was dying during font / annotation setup on the *next* unguarded modern API. Comprehensive sweep over pdfjs-dist@5.5.207's call sites against the Chromium 108 baseline turned up four more: URL.parse (Chrome 120+, 8 sites), ArrayBuffer.prototype.transferToFixedLength (Chrome 129+, 4 sites — used for right-sizing font-substitution write buffers), Uint8Array.prototype.toBase64 and Uint8Array.fromBase64 (Chrome 140+, used for image data: URLs and XFA payload decoding), plus Promise.withResolvers worker-side (Chrome 119+, 14 sites — would have been the very next crash, fires at module init around stream-handler setup). All five shipped as idempotent shims — src/frontend/src/polyfills.ts covers the main-thread path; the pdfjs-dist patch's top-of-worker hunk covers the real Web Worker (separate global). transferToFixedLength can't replicate detachment semantics from pure JS, so the shim copies; pdfjs drops the source ref immediately either way, so peak memory is unchanged. toBase64 is chunked at 0x8000 stride to dodge the String.fromCharCode.apply argv-length limit on large image buffers. Current Electron 35 (Chromium 134) was never affected by any of these — this is purely the Legacy BoardRipper-Legacy-macOS-x64-* zip (Electron 22 / Chromium 108, kept for macOS 10.15 Catalina). (0691836)Defensive fix for a PDF crash observed in an older Electron build —
verified against current Electron 35, lands as forward insurance against
future Chromium baseline lag relative to pdfjs-dist.
Promise.try is not a function no longer kills the PDF panel on older Electron. pdfjs-dist@5.5.207's worker message-channel dispatch calls Promise.try(handler, data) for RESOLVE / STREAM / PULL / CANCEL — four sites in pdf.js's own MessageHandler. Promise.try is ES2025 (Chrome 128+ / V8 12.8+). On older Electron Chromium baselines (the BoardRipper Legacy .app shipped earlier this year is one), the first message after worker boot throws and the PDF panel dies on open. Shim added in two places, both idempotent: src/frontend/src/polyfills.ts covers fake-worker mode (Electron file:// runs the worker on the main thread via dynamic import, shares the main global), and a new top-of-file hunk in the pdfjs-dist patch covers the real Web Worker case — main-thread polyfills don't cross the Worker boundary. Current Electron 35 (Chromium 134) was never affected; this is forward insurance against the next pdfjs-dist baseline ratchet. (4073325)New top-level interface knob: a global scaling factor that resizes every
chrome surface so the app reads well on dense laptop screens and 4K
displays alike, without touching board / PDF render resolution.
body { zoom: var(--ui-scale) } propagates the scale across the React tree and every portal mounted under document.body; the two heavy canvas hosts — .board-panel-canvas and .pdf-canvas-container — counter-zoom via calc(1 / var(--ui-scale)) so PixiJS and pdf.js keep rendering at their native pixel resolution regardless of the chrome scale. themeStore.scale persists under boardripper-ui-scale, clamps to [0.50, 1.50] in 5% steps, and applies before first paint so a reload doesn't flash at 100%. The slider commits on pointer-up only — the thumb tracks a local draft during drag — so the control doesn't rescale out from under the pointer mid-drag. Electron picks this up for free; the OS-level Cmd/Ctrl +/- still stacks independently. (0e012ee)Follow-up to v0.31.13. The load-overlay UI lockout had a second source
on the linked-PDF auto-load path.
LibraryPanel.handleOpenFile auto-loads bound PDFs right after the board fetch completes — and both calls flow through databankStore.fetchFileBuffer, which unconditionally fired loadProgressStore.start(). The PDF call wiped the board's in-flight state (sitting at "Building scene"), and since PDFs go through boardStore.addPdf rather than loadFile no one ever called finish() for the PDF, leaving the overlay open at "Downloading" until v0.31.13's watchdog fired 30 s later. Gated every loadProgressStore call in fetchFileBuffer on file.file_type === 'board'; PDF / schematic / image / future non-board fetches skip the overlay entirely. (3304386)Hotfix for v0.31.12 — multiple users reported the load-progress overlay
blocking the entire UI, including cases where the board was clearly
loaded behind it but the overlay refused to close.
failed, and there was no watchdog if BoardRenderer.activateScene failed to call back into finishIfMatching. The overlay is now a non-blocking bottom-right corner panel — pointer-events:none on the wrapper, auto only on the panel itself — with an always-visible X close button and a 30-second watchdog that force-dismisses on stalled-load state. A legitimate slow load still keeps the overlay open as long as any sub-step (downloading bytes, parser sub-phases, cache write) keeps poking setPhase / setPhaseDetail / pushLog. (bc0b31f)Pad geometry pass across every format that exposes it. TVW chamfered
pads draw their real outline, XZZ pads carry net-coloured rectangles
instead of generic circles, 2-pin overrides no longer halo. A clean-up
sweep on XZZ orientation made every X-fold board (A2338, A2681, most
Apple MLBs) display correctly, and the worst-case wrong files now stick
to a manual rotation override. Plus a new toggleable Copper-fills layer
for ground planes, a load-progress overlay so cold opens have feedback,
and the part outline / selection rectangle finally hugs the real pads
on resistor packs and overhanging-pad ICs.
surfacesLayer) follows the same emphasis machinery as traces. (e96077a, 2119ccf, b012c24)boardripper-file-view-prefs) records the user's rotation / mirrorX / mirrorY / flipAxis choices keyed on ${name}:${size}:${lastModified}. Auto-rotation still runs first, but the saved override wins. The XZZ auto-rotation heuristic gets right for ~all M1 / M2 MacBook boards now (after this release's fold-axis fixes), but a few outliers — A2338, A2681 — needed an extra 180°. Fix once via the rotate-180 toolbar button; subsequent opens auto-apply. Stays per-file so a fix on one MLB doesn't change another. (79cf870)buildBoardScene (where most of the wait sits) and auto-dismisses 1.5 s after the last phase completes. Diagnostic markers inside the scene builder report the cost of surfaces, traces, pads, the parts loop and grid flush so we can localise future slow-loads without re-instrumenting. (79242de, daae3a9)a94f55e)pin.padShape + padBounds. Chamfered pads (poly D-code) now trace the actual vertex outline instead of falling back to the rotated AABB; the poly extraction was capturing the wrong centre, fixed by emitting the shape-local verts verbatim. Trace-join cap: 'round' doubling at L-bends gone via segment chaining; T- and 4-way junctions still get a small spot, deferred. (9b8d598, c96b280).pcb files expose width/height/shape/rotation per pin in the 0x09 sub-block; previous releases only used them for the board.pads overlay, never on the Pin itself. Selection halo, pin sprite, and pad overlay now all trace the same outline. (1fc462d)pin.padBounds for 2-pin geometry when supplied, falls back to FlexBV synth for BVR/BDV/CAD/Mentor. (e41a379, 9cd6638)padShape: 'round' / 'square') no longer halo. Same fix as the natural path — the override branches were still sizing themselves from the synth-FlexBV eb rect, so any override on a 2-pin part recreated the doubling. Refactored so the pad rect comes from pin.padBounds regardless of which shape variant gets drawn. (9cd6638)min(padW, padH)/2. Pad overlay sits ABOVE the pin and covers it when toggled on; selection halo follows whichever shape is currently visible. (e865353, cce8d9f, 1720872)resolvePinColor) instead of the uniform warm-copper that hid GND/VCC distinctions on every file. Single-layer XZZ no longer skips the pad layer entirely — that was a regression from a partial-fix that hid the sidebar's Pads toggle. (335e8a4, b35e654, 5a7e664)padsTop.visible / padsBottom.visible. (1720872)computeEffectiveBounds was using the AABB of pin CENTRES only, so RP-series resistor packs and other parts with pads overhanging the pin centres had their outline + selection halo cutting through the pads. Now union of pin.padBounds first. Parsers without per-pin pad bounds (BVR/BDV/CAD/Mentor) hit the union as a no-op; identical to pre-fix. (c05e5bc)e72a562, March) set tab.mirrorY = true on load to compensate for the renderer doing the unfold. The parser has since fully unfolded the bottom half inline (p.x = 2 * fold.axis - p.x), so the leftover mirrorY became a free Y-flip. With 270° auto-rotation applied to tall boards (every X-fold result is tall), the Y-flip rotated into screen-X. Dropped the auto-mirror in all three sites — cache-load, fresh-parse, and syncMirrorsToDerivedFold. Y-fold boards (820-02016) were getting no auto-mirror in the load paths and displaying correctly; both fold types now match. (d989252)mirrorY=true un-mirrored them but every X-fold board then sat 180° rotated from where the user expects to see it. The parser's bottom-half X-flip leaves the geometry in a frame where 270° auto-rotation (standard for tall flipY formats) maps the screen's top 180° from the board's top. computeAutoRotation now returns 90° for X-fold + flipY (vs 270°). Y-fold path unchanged so 820-02016 still lands correctly. (3d0ae42)3fea5ad, May) was firing on chips whose pads are drawn at 45° but whose body sits straight on the board — UN/UF/UR/U-series ICs on A2442 and 820-02016. Added a perimeter test mirroring computeDiagonalOBB's axis-aligned guard: if pins on both a horizontal and a vertical AABB edge, the chip is axis-aligned regardless of what the pad angles say. (7146246)hasH && hasV check (≥2 pins on horizontal AND vertical edges) is geometrically enough — a true 45°-rotated chip touches the AABB only at four vertex pins (one per side, onL=onR=onT=onB=1) and that fails hasH/hasV. Dropped the threshold. (1a9ec35)drawPadShape when pins had padBounds, so clicking a pin under "Show pads off" snapped the halo to the pad outline — visually "revealing" the pad the user had just hidden. Gated on boardStore.showPads. (cce8d9f)board.surfaces survives the IndexedDB round-trip. SerializedBoardData schema never learned about surfaces, so first load populated them in memory (Copper-fills toggle visible) and every subsequent cache hit dropped the field (toggle gone). Three lines: add the field to the type, serialize, deserialize. PARSER_VERSION 68→69. (2119ccf)INFO[pdfindex] file extract failed lines didn't say which file or why; now both surface so a user with a single broken PDF in a 200-PDF library can identify it instead of grepping warnings. (49b04d2)XZZ pass: the parser now extracts the geometry that was sitting
unread in every Apple boardview — real pad shapes, per-part silkscreen
outlines, via drills — and renders rotated chips with rotated
outlines instead of axis-aligned boxes that miss the body. Plus an
issue-#19 UX fix so a PDF-driven selection respects your dim
preference.
showPads toggle. Pin radius now scales to min(padW, padH)/2 so N4090's 2,971 SoC balls draw at their real 6.75 mil instead of the hard-coded 8 mil that drew dots over real pads. PARSER_VERSION bumped 66→67 so stale caches don't hide the change. (aed68a5)SilkscreenPaths tagged with the part's side; the existing silkscreen overlay renders them. The chip-side rectangles now match real PCB silkscreen, not just an inferred bounding box. 17,428 per-part paths on A2442. (7d7c72d)board.silkscreen instead of being discarded. Chained via chainByComponent so the sparse art renders with few GPU draw calls. (33c8184)1, 5 on every surveyed Apple file with no observed variance — blind/buried stack-ups unrecoverable until a counter-example shows up). (e365add)computeDiagonalOBB's area-saving gate rejected the OBB: scattered thermal pads at the centre and a single "wing" pin at the AABB edge inflated the rotated rectangle until it saved less than 30% over the AABB and the gate fell through to the rect fallback. The format already records rotation per pad; on a tilted chip every pad's angle (mod 90°, since rectangles are centro-symmetric) lands in the same bucket. The parser now resolves that bucket once and stashes it on Part.angleDeg. computePartRenderPoly projects pins onto the recorded axis directly when set, bypassing PCA. board-scene's per-part border path flows through the same wrapper so the always-on body rect picks up the same OBB as the selection highlight. Detection gated on ≥70% pad-angle agreement so a chip with one oddly-rotated annotation pad still flips through, but a chip with a single rotated pad on a straight body doesn't get falsely tilted. (3fea5ad)56cdd26)c8fdb42)dimMode=off (#19). The Auto-dim-on-search switch (default ON) was overriding an explicit per-tab dimMode=off: clicking a designator in a PDF lit the full dim overlay even when the user had explicitly disabled dimming for that board. The auto-dim rule now only *promotes* a view that's already dimmed (Dim or Darklight); it never introduces dim against an opt-out. No setting migration needed. (df04f73)Hotfix for v0.31.9 — opening a PDF while 2-Window Mode was already on
silently added it to the popout but didn't raise the popout window, so
the user looking at the main window saw nothing happen.
ensurePdfPanel correctly drops the new panel into the popout group as the active tab — but Chromium does not raise the popout's window automatically, so the user sees no visible change in the main window and concludes the open didn't work. Fixed by calling popoutGroup.api.location.getWindow().focus() after the addPanel call. The lazy-popout path (no existing popout) already raises naturally via window.open(). Added E2E B8 covering "second PDF while popout exists" with an active-tab assertion. (ba82ddf)2-Window Mode: a single toolbar toggle that pops the entire PDF group
out into its own browser/Electron window so the board can live on one
display and the schematic on another, with the click-board → jump-PDF
link still working across windows.
IconLayoutBoardSplit (single-window, internal splits) and IconBoxMultiple (multiple stacked windows) to communicate state. (6a8f7d2, 8e3963e, 6fa6069, f69e011, cccd299, 193cb52, f726735, 9bae652)6fa6069, cccd299)<body> mirror into every popout window via a MutationObserver, so accent/dark-mode changes propagate without a manual refresh. Global keyboard shortcuts (Cmd-F, etc.) attach to every popout's document via a 500 ms poll so they fire when focus is inside the detached window. (fe56c12, 8b549c3)mainWindow.webContents.setWindowOpenHandler overrides the spawned BrowserWindow's options so the PDF popout has a native title bar ("BoardRipper PDF"), no application menu, the BoardRipper icon, and the same preload script as main. popoutUrl is set to a relative path (popout.html) so it resolves correctly under both http:// (browser dev) and file:// (packaged Electron). (cccd299)App.tsx's onDidRemovePanel handler calls pdfStore.closeFile() plus board-binding cleanup whenever any pdf-* panel is removed — designed for the user clicking the panel's X. But the 2-Window-Mode redock path uses panel.api.close() purely as a move-mechanism (Dockview's panel.api.moveTo only accepts a target group, and there isn't always a same-window PDF group to move into). That fired the same cleanup, destroying the pdf.js doc and clearing bindings, so after toggling off the panel came back but rendered empty. Fixed via an isRedockingPdf(name) predicate exported from dockview-api.ts; the panel-remove handler short-circuits cleanup while a name is in transit. Both the drag-drop and file-picker open paths flow through the same code, so both are covered. (90d498c)193cb52)tests/two-window-mode.spec.ts + tests/two-window-mode-store.spec.ts. Cover sections A (toggle state machine + multi-PDF migration), B (lazy popout), D (window-lifecycle: OS-close, reload-with-mode-on), H (persistence + corrupt-localStorage), I (mixed-group safety: non-PDF panels parked in a PDF group stay in main), J (mode-OFF regression). PDF fixtures generated in-memory via pdf-lib so the suite runs without samples/. D13 uses popup.evaluate(() => window.close()) instead of popup.close() to reliably fire the beforeunload event Dockview hangs the popout-close chain off. (f0f75d2, e05d5ad)PDF open path broken on older browsers — single-edit hotfix to the
vendored pdf.js worker.
loadFile failed: hashOriginal.toHex is not a function on Chrome < 136 / Firefox < 132 / Safari < 18 and legacy Electron forks. pdf.js 5.5 calls Uint8Array.prototype.toHex (Chrome 136+) directly inside the worker's fingerprints getter, and Map.prototype.getOrInsertComputed (Chrome 134+) in 5 places across font/AcroForm/XFA caches. The main-thread polyfills in pdf-store.ts don't cross the Web Worker isolation boundary, so every PDF open crashed on those clients. The vendored-pdf.js patch (patches/pdfjs-dist+5.5.207.patch) now prepends an idempotent shim block to pdf.worker.mjs covering both methods. Electron and modern Chromium are unaffected (the typeof !== 'function' guard keeps native impls). Sixth edit added to the existing patch; patches/README.md updated.Settings panel cleanup pass plus a new global search inside Settings.
Mechanical-part auto-detection extended and made calmer. Desktop
artifacts now mirror to ripperdoc.de alongside the landing-page rework.
/ to focus) that lives-filters every Slider, Toggle, section heading, and subsection by label + tooltip + curated keyword synonyms. Tab pills show per-tab match counts so cross-tab discovery works; sections with no matches hide; sections matched by name auto-open and show all their controls. Esc or the ✕ button clears. (95fb2a7, fa2781f)[primary] → [adjacent]. Both fields were already read by the renderer but had no UI. (27dae0e)2db9073)PARSER_VERSION bumped so stale IndexedDB caches don't hide the change. (ca318e9, a5eebe8, 415b4de)selectionHalo field dropped. (d0b9ebe)d0b9ebe)e53fd6c)d0b9ebe)47154eb)Worklist gains first-class net support — pin a net to a worklist from
the search tab or right-click, mark it short/solved/absent, optionally
flag it with the lightning-bolt surge tag.
IconPin button (turns into IconPinFilled + accent tint when already pinned to the active worklist). Clicking pushes the item to the active worklist, auto-creating one if none exists. Click stops propagation so the row's existing focus action still fires when you click the body. (ebb88a4)Worklist data model gains a netEntries: NetWorklistEntry[] array parallel to the existing parts list; old persisted records hydrate with [] (back-filled in resolveEntries, no schema migration). Each net entry carries a mark, note, and an optional surge flag — the signal analogue of waterdamage, indicating an over-current / ESD event, displayed as a lightning bolt. A subtle "Nets" sub-heading separates the two sections in the panel when both are populated. (ebb88a4)NetWorklistMark type, not the part replaced/reworked/cleaned vocab — those describe physical actions on a component and don't map onto a signal. Short = fault identified (red IconAlertTriangle); solved = resolved (green IconCheck); absent = net not present / not reaching (slate IconUnlink). The type system prevents the part vocab from leaking into a net entry. (bdc0552)IconPin → IconPinFilled toggle as the component chip. Click to add the net to the active worklist; click again to remove. Auto-creates a worklist on first use, opens the Worklist sidebar tab, and surfaces a toast. (bdc0552)pushNets, pushNetToActive, removeNetEntry, setNetMark, setNetNote, cycleNetMark, toggleSurge. Parallel to the existing part-side methods. Net names are case-canonicalised against the loaded board on push so the entry stays in sync with the board's casing. (ebb88a4)NET_MARK_* tables in WorklistPanel.tsx (NET_MARK_ICON, NET_MARK_SHORT_LABEL, NET_MARK_TITLE, NET_MARK_BTN_COLOR) mirror the existing part-side tables. WorklistNetRow is structurally identical to WorklistRow apart from the icon set + handler routing. (bdc0552)qa-worklist-net testid on the context-menu net chip parallels the existing qa-worklist-part, enabling future regression tests to assert against the net chip independently. (bdc0552)multiHighlightGfx system only paints part outlines. Layering a worklist-net highlight on top of the existing single-highlightedNet machinery without colour collision is a larger refactor — net worklist entries are panel-only for this release. Clicking a net-row in the worklist focuses the net via the standard focusNet path, which respects the user's Zoom Mode setting from v0.31.5.Sidebar Search tab redesign, configurable navigate-to-component zoom, disco
mode polish, and a green-CI cleanup that knocked 91 lint warnings off the
codebase. Same-day follow-on to v0.31.4.
boardStore.selection so clicking a net inside an expanded component (or vice-versa) no longer collapses the spoiler. (2893d19, 9e9145c)845b0a0, e483f6b, 44b3a0b)e134f4b)zoomToBounds that silently clipped navTargetSize on small boards. Caught by a new Playwright test (tests/nav-target-size.spec.ts) which failed on first run with scaleSmall === scaleLarge even with the target doubled in *Always* mode — the cap was masking the setting. Kept only the 6× absolute ceiling. (e483f6b)IconGhost3 (a stripped-down ghost with no smile), which read as a different shape vs the *on* state's IconGhost2. Now uses IconGhost2 for off/ghosts (off conveyed purely by the absence of .active — no slash/strikethrough variant) and IconGhost2Filled for disco (the existing .disco-active hue-rotate animation gives it the flashing-ghost feel). (e135e11, 7b0e420)44b3a0b)react-hooks/set-state-in-effect errors (in FZKeyDialog.tsx and WorklistPanel.tsx) and an orphan computeFitToBoardScale had been silently failing lint-and-typecheck for ~7 days. Releases worked because the release pipeline doesn't depend on CI; user testing surfaced it. (b9ffb09, 7b0e420)SettingsMockup and SettingsPanel/PartTypeRow — the ref pattern was rewritten using React's official "previous-render state" pattern (useState + render-time comparison + setState).PdfViewerPanel callbacks (syncTransform, rescaleWrapperChildren missing from useCallback/useEffect arrays — real correctness issues, not just lint noise).Sidebar.utils.ts, BoardSidebar.utils.ts, panels/board-viewer-bridge.ts.any casts at pdf.js boundaries replaced with structural as unknown as { ... } casts; pdf.js / type-decl mismatch documented inline.react-refresh/only-export-components (helpers exported alongside React components in three panel files) — pre-existing structural refactor, out of this round's scope. (2016512)tests/nav-target-size.spec.ts exercises (a) Always mode + navTargetSize change → proportional viewport scale change, and (b) Keep mode preserves scale across navigate. Exposes a __renderSettings window hook in DEV builds alongside the existing __boardStore / __boardRenderer. (e483f6b)Two new things you can play with: an interactive first-run gesture setup, and a
disco highlight mode on the ghost-toggle button that pulses every same-net part
red on both sides.
recommendSetting. The opposite action auto-fills, then it advances to the next surface. OS-momentum is detected live so BoardRipper's own glide stays OFF when your trackpad already provides inertia (no compounding). Re-openable from the start page's Quick settings ("Set up by gesture (interactive)"). The Playwright suite isn't affected — auto-show is suppressed under navigator.webdriver. (5c06d4d)prefers-reduced-motion. (76701ce, 3294553, 82d3085, 228a841, ee14b99).xzz (wrong — XZZ PCB is .pcb) and omitted .bv. Updated with dual-purpose notes for .brd / .cad / .bdv. (5c06d4d)recommendInertia direction stays in lock-step with the welcome checkbox semantics. (5c06d4d)expandPoly / drawPoly / drawPartOutline are now module-level so cross-side ghost + disco halo share the part-shape primitive instead of open-coding the poly/AABB dichotomy. Introduced a GhostMode type alias to replace the inline 'off' | 'ghosts' | 'disco' union across the seven sites that referenced it. (aa10364)renderDiscoHalo skips path building during the ~70% silent window of each cycle and only fires needsRender on the transition into silence, tracked via discoHaloDirty. The ticker now gates renderNetLines on netLineMode !== 'off' && highlightedNet, so disco-only frames don't pay for a net-line clear. (aa10364)A roundup release: new multilayer trace-layer controls, PDF viewer default and
zoom refinements, two format-robustness fixes (PADS rejection, TVW parts
recovery), and a batch of audit-driven backend/UI hardening.
67dcd9c, b2a0e09, 85b2650)1b50386, c965a31).pcb files are now rejected with a clear message instead of the cryptic XZZ: invalid header offsets. The .pcb extension is shared with the supported XZZ boardview format; PADS Layout's native binary database (magic 00 FF 26 20) is detected and refused up-front. (43b7d64)315e0ae)d0136ec)ac94e40)deb43ef, 42c2d22).dockerignore, CI Go 1.25, release-signature self-verify, and a native-amd64 boot smoke-test gate before publishing. (271ea48, 048f33e)da41437, 657a9cf)The real fix for the v0.31.0 update failure. v0.31.1 turned out to address the
wrong cause — this release makes the update actually succeed on installs with a
large existing library. As before, no install was harmed: the orchestrator
healthcheck rolled every failed update back to the prior working version.
pdf_text (FTS5) / pdf_pages tables, whose content moved to the separate pdfindex.db. On a populated library that drop (a) opens a SQLite *statement journal* — but the scratch image has no /tmp and the runtime CWD / isn't writable by UID 65532, so SQLite returned SQLITE_IOERR_GETTEMPPATH (disk I/O error (6410)); and (b) frees hundreds of thousands of pages, taking ~60 s — which blocked /api/health past the updater orchestrator's 60 s healthcheck and got the update rolled back. Two-part fix: point SQLite's temp files at the always-writable data volume (SQLITE_TMPDIR=/data), and split the migration so only the fast pdf_donors table is created on the boot path while the heavy legacy-table drop runs in a background goroutine after the server is serving. Boot-to-health on a real 1.5 GB database dropped from a rollback-inducing >67 s to 24 s. Verified against a copy of the real failing database on amd64 hardware. (7da5bc3)modernc.org/sqlite bump (v1.34.5 → v1.50.1, for Go 1.25) is retained as worthwhile dependency hygiene, but it did not fix the boot failure — both v0.31.0 and v0.31.1 failed identically on a large database. The cause was the temp-dir / migration-timing issue fixed here, not the SQLite driver version.Hotfix for v0.31.0, which failed to boot on linux/amd64. No user was left
stranded — the orchestrator's healthcheck rolled every failed update back to the
prior working version — but v0.31.0 could not be installed. This release makes
the update succeed.
Failed to open databank: unable to open database file: out of memory (14). v0.31.0 bumped the Go toolchain to 1.25 (required by the new pdfium/wazero PDF indexer) but left modernc.org/sqlite pinned at v1.34.5, whose transpiled libc (v1.55.3) was generated for Go 1.21. Under the Go 1.25 runtime its memory layer failed at the first mmap, so SQLite could not open databank.db and the server exited before serving /api/health. Bumped modernc.org/sqlite to v1.50.1 (libc v1.72.3, regenerated for Go 1.25), kept in lock-step with the golang:1.25 build image. Verified by an isolated linux/amd64 boot test on real hardware. (c4e2482)$TARGETARCH from $BUILDPLATFORM instead of QEMU-emulating the target toolchain during multi-arch buildx — faster and removes an emulation variable from the build. (c4e2482)Library-wide PDF full-text search rebuilt on a real backend index, automatic
content-deduplication folded into the scan, streaming search results, and a
Brand → Model → Board# reorganisation of the Board# view.
pdfindex.db (FTS5 external-content, porter+prefix tokenizer) built by a pooled pdfium-via-wazero engine with a per-file kill timer; the container stays CGO-free/scratch. Opening a PDF auto-indexes it via a client-side fast-path so it's searchable immediately; an autonomous backend indexer with a priority queue + stale-row watchdog handles the bulk. (ba6f7c9, eb5ef97, fe99c03, 97e6399, 8846388, 0076fbc, 1fd9860, 9faeed1, d8c2445, 3bb915a)GET /api/databank/search/stream emits one result per file as the FTS cursor finds it (NDJSON), so the list builds up immediately with a "Searching… N found" indicator instead of an ~8 s freeze. Results are one row per file with a hit count, single-click selects (info pane), double-click opens and jumps to the match. (37c42a8, f9739ec, 051d5ac, 4b89a6e, 182464a)f602566, a67bf8e, 1457c11, b622427, 16bd267, ca1125d)77c1b96, f94134c, 3168ffa)AOZ5332 → AOZ5332QI). (8bc3ebe)sha256(size ‖ full-file ≤192 KiB else head/mid/tail 64 KiB)) — a unique byte size is never read. (51311e7, 34920e1, 86ea2f1, 58f0094, b92d939)08f2b6f)815bdeb, 88c007d, 3d5df43)f152df3, 2be750e, b3e6e4b, c2c814d, 8844394, 2525c13)pdfindex.db (O(1), not a multi-minute row delete) and is no longer gated on a running file scan. (61f626d, 6b8c514, 10a0c3a, 9055b75)85c2e03, f485694)auto_bind) — the O(boards×pdfs) match loop was adding hours to large-library scans. Scan status now updates live during the long walk/compare phases. (a937f2d, e296fbc, fb828c9)Cross-lookup between two linked PDFs — for boards that only exist as PDF — plus
copy-to-clipboard for the current selection, drag-dropped files filed into the
library, and an FZ unit-detection fix.
62297b1, ecff231, 6191dca, de9c710)de9c710)c00e1af)incoming/ instead of living only in the browser session, so a dropped file is kept for later. (224a00a)UNIT:millimeters on mil-coordinate files. Some ASUS .fz files declare millimetre units while their coordinates are actually in mils; the mislabel scaled the board down ~25×, rendering pins sub-pixel ("opens but no pins/nets"). Unit detection no longer takes the header at face value on such files. (0a72b40)Worklist connection-highlighting and a more capable hierarchical net-line mode,
plus GenCAD bottom-side placement fixes and a clearer error for encrypted
BRD_V1.0 boardviews.
aad93b1)aad93b1)0bd6f0e, 0836107)0836107)SHAPE … MIRRORY). Allegro2CAD .cad exports store bottom components with a MIRRORY FLIP shape flag and shape-local pins; the parser dropped the mirror token, X-flipping every bottom footprint about its placement origin — 1006 of 2900 parts (~35%) misplaced on the Dell XPS 9560 LA-E331P. The mirror is now applied in shape-local space before rotation; verified against an independent world-coordinate export, all 7,579 bottom-side pins match to <1 mil. (3e475f3).cad inherits the same side-labelling quirk as Allegro .brd, so the parser now applies the identical pin-majority heuristic (>55% of pins on the declared bottom side ⇒ flip the primary side). The Dell board (62% bottom pins) now matches its source .brd. A $BOARD-derived real outline was prototyped in the same investigation but reverted — $BOARD content is too exporter-specific to stitch reliably — so the synthetic rectangular outline is unchanged for now. (5308dc4, 2619e17)BRD_V1.0 container (e.g. ASUS TURBO-RTX3080) failed with a misleading "BDV file … may be corrupt or empty" — its fully-encrypted body matched no format and the .brd fallback handed it to the wrong parser. Detection now recognises the 8-byte BRD_V1.0 magic and routes to the BRD parser's friendly "proprietary, encoded format — support may be added" message. (b9ec51e)Two parser fixes surfaced by ASUS G513R laptop boards (FZ and GenCAD exports
of the same 6050A3348801/03 design), plus a Landrex theme refinement.
GCVN magic and a 0x78 0x9c zlib header, but the body is a raw DEFLATE stream that zlib-mode inflate rejects ("invalid distance too far back") — and on the bounded content slice it silently inflates to empty rather than throwing, so the file fell through to "no parts or pins" despite a valid key. The content-decompress step is now an ordered set of fallbacks (standard zlib → descrSize+4 zlib for the GOCCANH-XJ tail chop → raw deflate skipping the zlib header), each accepted only if it yields non-empty text. ASUS G513R 6050A3348801 now parses (4138 parts / 2936 nets); standard Acer N22Q22 FZ unaffected. (16c4935)$DEVICES PART description as component value + serial. The parser ignored the $DEVICES catalogue, so ComponentInfo showed only the COMPONENT's inline DEVICE field — which on Mentor CAMCAD exports (Compal/ASUS boards, e.g. G513R 6050A3348803) is just "Device <refdes>". The real BOM data (device type, value, package, manufacturer, MPN) lives in each device's PART line. We now parse $DEVICES into a device→PART map and split the PART string on //: the left side becomes the value (e.g. "RES 200K OHM 1/20W (0201) 5%"), the right becomes the serial (e.g. "TA-I/RM02JTN204"). Falls back to the inline device name when no PART exists, dropping the placeholder, and skips per-refdes shape names that just echo the component name. (e6375e0)ui block mirrors the default theme, so the toolbar, panels, accent, text, and library badges stay put. Every board label is forced white: part labels (was gray) and net labels (was blue) were static palette entries that ignored the theme, so they're now theme slots, set white under Landrex for maximum contrast against the black canvas. Pin labels already followed the theme. (f92c0e5)Allegro parser fixes surfaced by a Dell Nvidia Quadro 5000M board
(Nvidia_5000M_Dell.brd) — the first v17.4, millimetre-units file in the
corpus — plus the PDF pan-boundary follow-up.
boardUnits when scaling coordinates to mils. The assembler divided every coordinate and dimension by unitsDivisor alone, implicitly assuming MILS. That is correct for the ~10 MILS boards we had, but silently mis-scaled the two metric files: a millimetre-units board (divisor 10000) came out 39.37× too small — a 5.2″×3.6″ board collapsed into a ~3 mm blob where every part overlapped into one tiny mass. Fold the per-unit mils factor into the divisor at the single source point so all downstream conversions land in mils; MILS files are byte-for-byte unchanged. Also corrects a µm-units file that was silently oversized. (c3d3157)0x2D footprint-instance layer byte 0 for every part regardless of side, so the Nvidia board reported 838 top / 4 bottom when it is really ~389 top / ~453 bottom — every bottom-side part rendered on top. Allegro also records side on each footprint's PACKAGE_GEOMETRY assembly drawing via the 0x14 graphic layer subclass (0xF7=top, 0xF6=bottom); that subclass agrees with the instance flag on every other corpus file where it appears, so prefer it when present and fall back to the instance flag otherwise. (05604fe)extractVias walked every 0x33 block, including footprint-library via-in-pad templates whose net pointer references the parent 0x2B footprint definition (not a 0x04 net) and whose coords are footprint-local. They rendered as a phantom BGA-style via grid at the board origin — ~2.8k vias (16 GDDR chips × 170 balls) on the Nvidia board, plus smaller clusters on Compal LA-H271P and Dell XPS 9560. Templates are now dropped. (7288903)extractTraces picked up 0x05 ETCH tracks whose netAssignment points at a 0x2B footprint def, dumping ~340 uniform local-coord segments as a phantom cluster at the origin. Both sites now share one pointsToFootprintDef() check. (3f30b7b)clampPan now returns immediately unless the new Settings ▸ Performance & Debug ▸ "PDF Pan Boundaries" toggle is on; the page-flip thresholds in the wheel handler still fire as the user crosses them. The zoom range stays at the historical 0.5×–10×. (878c74d, 340bee1)Two self-update papercuts found while shipping v0.30.8.
docker-compose.yml now mounts /var/run/docker.sock. Without this mount, in-app Update & Restart is a no-op (the running container can't call the Docker Engine API to pull and swap itself), but nothing in the UI explains why — fresh installs that used the shipped compose ended up silently stuck on whatever version they originally pulled. Combined with the existing user: "0:0" line, this makes self-update work out of the box. Pre-existing installs need to add the same line and docker compose up -d. (b8bfcab follow-up)latest-update.tar and *.brupdate.** The previous isUpdateBundle regex required the filename to start with boardripper-update-v[0-9], which silently rejected the stable-alias filename our releases also publish. Loosened to accept the alias plus any file with our own .brupdate extension. (b8bfcab)boardripper-v0.30.X.tar.gz (the Docker image, for docker load) and boardripper-update-v0.30.X.tar (the signed update bundle, for drag-drop) side-by-side. The image alphabetises first, so it's the easy mistake. Dropping the image now raises a toast pointing at the right filename instead of falling through silently. (b8bfcab)b8bfcab)Right-click context menu redesign and two parser fixes.
title attribute). The whole menu reads ~30% shorter at the same content. (ac6fcc8)ac6fcc8)IconPinFilled on .is-lit), tooltip switches to "click to remove", and the click removes the entry — mirrors shift-click on the canvas. (ac6fcc8)ac6fcc8)ac6fcc8)IconMinus retired for the "no mark" worklist row. Reads as "subtract"; replaced by a dim · (opacity 0.4). The mark-cycle popover already names the new state on click, so the row's idle look stays calm. (ac6fcc8)BRD_V1.0 container. New 16-byte ASCII header check at the top of parseBRD throws with a descriptive message: "proprietary, encoded boardview format. Decoding is under active investigation — support may be added in a future release." Without this gate, OpenBoardView-style decoding ran against the encoded body and emitted garbage geometry that took users a while to diagnose. (8fe1b94)descrSize. The Vietnamese GOCCANH-XJ converter writes descrSize 4 bytes longer than the canonical layout, which chops 4 bytes off the deflate stream and makes pako abort with "unexpected end of file". When the canonical slice fails, the parser now retries once with contentEnd + 4 before reporting failure — symmetrical to the trailing-pointer trim already in place on the forward path. Fixes silent failures on a small but real set of community boards. (8fe1b94)PDF watermark wand toggle — fixes a long-standing bug where the user's custom watermark term list was destroyed on every wand-off→reload→wand-on cycle.
pdfWatermarkFilter: [] to localStorage and stashed the previously-active list in a non-persistent useRef. On reload the ref reinitialised from globalSettings.pdfWatermarkFilter which was now [], then fell through to a hard-coded 5-term fallback. Users who had edited their list (added a vendor watermark via right-click) lost those edits on every off/on cycle; users who toggled off and never toggled back on saw their full list as empty in Settings indefinitely with no auto-recovery path. Fix: split the on/off state into a new persistent pdfWatermarkFilterEnabled: boolean field — pdfWatermarkFilter now ALWAYS represents the user's list, the wand button only flips the flag. (e664291)getActiveWatermarkFilter(settings). Returns the list when enabled, [] when disabled. Routes all four consumers through it: the two page.render() call sites in renderPageToBitmap and renderTiledPage, the operator-list dispatch in pdfStore.openDoc, and the click-test in mapClick. Eliminates the "is the filter active?" question being answered by filter.length > 0 four different times. (e664291)pdfWatermarkFilter: [] and no pdfWatermarkFilterEnabled key get auto-bumped to pdfWatermarkFilter: <5 current defaults> + pdfWatermarkFilterEnabled: false. Toggling the wand back on then brings the 5 defaults back. Users with a non-empty list and no flag set get enabled = true (matches the pre-fix semantics where non-empty = active). New users get the 5 defaults + enabled = true via DEFAULTS. (e664291)pdfWatermarkFilterEnabled: true alongside appending the term. Previously, adding a term while the wand was off silently extended the list with no visible effect. (e664291)FZ format — removed the bundled ASUS RC6 decryption key. Users now obtain it themselves through a small in-app dialog (fetch from public GitHub mirror or paste).
DEFAULT_FZ_KEY deleted from fz-parser.ts. The 44 × uint32 key required to decrypt encrypted ASUS .fz boardview files is third-party material BoardRipper did not author and has no license to redistribute. Upstream OpenBoardView takes the same position — FZFile::getBuiltinKey() returns an empty array — and we now match. The parser still ships the parity-check fingerprint (also from OBV) so any user-supplied key is validated before use. Distributing the key alongside the binary raised exposure under anti-circumvention statutes (DMCA §1201 in the US, InfoSoc Directive Art. 6 / CDSM in the EU) even though the parsing logic itself is MIT-derived; cleaner posture is to have the user fetch it themselves. (b09c777)FZKeyError with 'missing' | 'invalid' reason. Encrypted file + no configured key → 'missing'; configured key produces non-zlib output after RC6 → 'invalid'. The board-store catches both and opens the FZ-key dialog; the 'invalid' path additionally clears the bad stored key so the next fetch/paste replaces it without manual cleanup. (b09c777)components/FZKeyDialog.tsx). Opens automatically the first time a user drops an encrypted .fz file. Two paths: Fetch pulls from the public mirrors at github.com/cryptonek/illegal-numbers (primary) and github.com/yliu-d/illegal-numbers (fallback) in order until one yields a parity-valid key; Paste accepts any text containing 44 hex tokens. Both run through validateFZKey() before persisting to localStorage (boardripper-fz-key). A cyrozap/pcbrepair-rs GitHub mirror is intentionally excluded from the fallback list — its FZ_EXPANDED_KEY[43] = 0x0945692e is corrupted (the trailing e was appended to fix Rust syntax around the truncated 0x0945692 in upstream cryptonek; the canonical zero-padded value is 0x00945692, the only one that passes parity). A collapsible "Why isn't the key bundled?" section in the dialog explains the legal reasoning in plain language. (b09c777)store/fz-key-store.ts. New singleton store extending Emitter for useSyncExternalStore integration. Exposes getFzKey(), setKeyFromText(), clearKey(), fetchAndApply(), plus a promise-based ensureFzKey() gate the board-store awaits before retrying an encrypted-FZ parse. parseFzKeyText() is regex-based (0x[0-9a-fA-F]{1,8}|[0-9a-fA-F]{8}), so the same parser handles cryptonek's markdown table, raw hex pastes, and Rust array literals indifferently. (b09c777).library-modal-* chrome (backdrop, modal box, field rows, action row, primary-save button). Net CSS addition: ~15 lines for a wider modal variant, a textarea that matches .library-modal-field input, and two inline-message colours. (b09c777)docs/formats/FZ_FORMAT.md and THIRD_PARTY.md updated. THIRD_PARTY.md now carries a dedicated *FZ decryption key — not bundled* entry that documents the posture and points users at the in-app dialog. The OpenBoardView attribution clarifies that the key is not part of what we inherit from upstream. (b09c777)Migration note for existing users. After updating, the first time you open an encrypted .fz file you'll see the new dialog. One click on Fetch restores the previous behaviour. Unencrypted .fz files (the rare case where the raw zlib stream is already at offset 4) continue to open without prompting.
PDF tile render path — single perf change with measurable settle-time win on fresh-tile renders.
createImageBitmap with the next tile's page.render(). The tile loop in renderTiledPage used to await BOTH page.render() and createImageBitmap strictly in sequence per tile, leaving the pdf.js worker idle for the duration of each bitmap pump. The new structure holds each iteration's bitmap promise as pending and collects it at the top of the next iteration — meaning tile N+1's page.render() is in flight in the worker while tile N's pixels are being pumped into an ImageBitmap on the main thread. page.render() itself stays strictly serial per pdf.js's constraint (parallel calls on the same page produce flipped/mirrored tiles); only the JS-side bitmap creation overlaps. Cancellation drains the in-flight bitmap before returning so the LRU cache still gets valid pixels rendered before the cancel. Expected ~30–50% reduction on all-fresh-tile settle paths (zoom change, page change); no impact on cache-hit pan paths. Directly addresses the observation that Standard mode's crisp settle felt faster than Tiles mode's — the 2N awaits in sequence were the structural cause. (40edb70)PDF viewer feel — two complementary changes aimed at making zoom and multi-page navigation smoother on heavy schematics.
shouldUseTilesRef predicate that reads both mode + zoom, replacing the four scattered zoom > 1.05 literals; mode flips re-route the next render without a React-deps refresh. (b84510a)scheduleTierRender are now paused while a trackpad pinch / Ctrl+wheel zoom is active (150 ms self-expiring gestureActiveRef set in markGestureActive). The 60 ms trailing debounce still fires once the burst ends, so the user sees CSS-transform-only motion during interaction and one crisp render at settle — same model Firefox's PDFViewer uses. Touch pinch and Safari gesture* paths already had this implicitly (neither calls scheduleTierRender mid-gesture); this brings the wheel-pinch path in line. (b84510a)a876c74) was masking a boundary-bounce glitch, but the side effect was that at zoom ≥ 1 each page is taller than the viewport and the wheel-pan flip threshold at containerH/2 takes many wheel events to reach — felt like being "stuck" on one page mid-document. Restored the zoom-out-to-see-adjacent-pages workflow across all four zoom paths (wheel, Safari gesturechange, touch pinch, keyboard). 50% is far enough to see neighbours without re-surfacing the original glitch. (e5ef926)The headline feature is a complete rewrite of the PDF watermark filter: it now runs *inside* pdf.js (via a patch-package-managed patch) and drops watermark glyphs at parse time instead of at render dispatch. Plus a stack of Worklist polish (waterdamage flag, ticket note, custom soldering-iron icon) and a couple of tooltip / hover-info improvements from earlier in the session.
[water] token on the row (R12[replaced][water] (note)). (762cfe5)Ticket note ▸ spoiler at the top of the active worklist holds a free-form note (~4 KB cap). Preview of the first line shows when collapsed. Roundtrips through the clipboard via > -prefixed lines immediately after the -[name]- header. (762cfe5)position: fixed could land in the wrong place when an ancestor Dockview/sidebar wrapper created a containing block. Now portaled to document.body so position: fixed coords always reach viewport space. (762cfe5)IconSolderingIron component in src/icons/: iron body from mdi:soldering-iron (Apache 2.0) with the cord subpath dropped and horizontally mirrored; smoke wisp hand-traced inspired by game-icons:soldering-iron (CC BY 3.0). Replaces the bandage emoji-substitute. Attributions in THIRD_PARTY.md. (762cfe5)4bdc9ad)The v0.4.2 – v0.30.2 implementation passed a Set<number> of pre-computed operator indices into pdf.js's public operationsFilter render callback. That had two latent bugs that manifested on more PDFs as the filter list grew: pdf.js's getOperatorList uses NullOptimizer (raw operator stream) while render uses QueueOptimizer (merges/reorders ops), so pre-computed indices didn't line up; and any PDF that emits one showText per glyph for sub-pixel positioning (Gigabyte schematics, for example) never matched substring filters per-op anyway.
The new design lives inside the pdf.js worker, via a patch-package-managed diff at src/frontend/patches/pdfjs-dist+5.5.207.patch. The patch:
watermarkFilter: string[] option to PDFPageProxy.render(...), forwarded through _pumpOperatorList → GetOperatorList worker message → Page.getOperatorList → PartialEvaluator.getOperatorList.BT and ET, tracks every showText op's args reference and the accumulated glyph-unicode string. Ops flow through operatorList.addOp in source order — no buffering, no reordering — so async-emitted state ops (setFont) land where pdf.js expects them.ET, NFKC-normalises the accumulated string + each filter term, lowercases, strips whitespace, substring-matches. If any term matches, retroactively sets each tracked showText's args[0] = [] so the op still executes but draws nothing.Trade-offs and why this shape: we tried a per-op filter (broke on per-glyph PDFs), a whole-BT-buffer-then-emit approach (broke rendering whenever a real text block went through the buffer, because state ops emitted async to the operator list landed before the buffered BT did), and finally landed on in-place glyph-array zapping after the fact — the only approach that preserves pdf.js's strict op-stream ordering while still letting us decide at BT-block granularity.
Notable shipped fixes inside this rewrite:
fi (U+FB01) decompose to f + i. Without it, "Vinafix.com" never matches the user's Vinafix filter term. Click-test path (isPdfWatermarkText in render-settings.ts) uses the same rule, kept in lock-step by design.cMapUrl + standardFontDataUrl wired into every pdfjsLib.getDocument call. Some donor PDFs (Gigabyte schematics) ship fonts that reference CJK/vendor CMaps; without these URLs, pdf.js's font loader fails with Ensure that the cMapUrl and cMapPacked API parameters are provided and the glyphs arrive at the operator stream with no .unicode — the filter then has nothing to match against. Dev server points at unminified pdf.worker.mjs so the patch targets readable source; vite still minifies for production.flushOperatorListCache(fileName) on filter toggle — pdf.js's intentStates cacheKey doesn't include the filter, so a toggle alone leaves the cached operator list in place. We now call page.doc.cleanup(true) to force a re-parse with the new filter.self. instead of this. in the showText branch. The switch sits inside new Promise(function promiseBody(resolve, reject) { … }), a regular function — this is undefined in strict-mode module scope. The earlier this.watermarkFilter read threw a TypeError that pdf.js's ignoreErrors catch silently swallowed, dropping the whole operator list, which manifested as "no text renders at all". Caught via Playwright probe.pdfWatermarkFilter; the existing filter-change subscription flushes caches and triggers a re-parse with the new term.Vinafix, www.chinafix.com, www.xinxunwei.com, notebookschematics.com, notebook-schematics.com. Migration recognises any prior default list and upgrades automatically; explicit customisations are preserved.The patch survives npm install / Docker builds via a postinstall script wired into src/frontend/package.json. Updating procedure for pdf.js version bumps is documented in src/frontend/patches/README.md.
PartMeta (primarily TVW; partial coverage from BVR/BDV/Allegro), a new line appears between R123 · pin 2 and any OBD readings, joining value and package with · (e.g. 10uF · CHIP0603R). Hidden entirely when both are empty so non-TVW tooltips stay compact. Matches what ComponentInfoPanel and BoardSidebar already show. (07f018f)Top · pin trace (awkward). Now trace · Top (or just trace when no layer name). Net stays on line 1. (07f018f)localhost. navigator.clipboard.writeText is only defined on secure contexts (HTTPS or http://localhost); the dashboard accessed at http://192.168.x.x:1336, Vite's network URL, or a Tailscale 100.x.x.x address left it undefined and the copy threw Cannot read properties of undefined (reading 'writeText'). A new copyText() helper in src/clipboard.ts falls back to a transient off-screen <textarea> + document.execCommand('copy'). (4bdc9ad)BoardRenderer.teardownForReinit removes multiHighlightGfx alongside the other highlight layers — was missing from one of the two teardown paths, leaking the graphics object on tab switch. (762cfe5)localhost but failed over LAN / NAS / Tailscale. navigator.clipboard.writeText is only defined on secure contexts (HTTPS, or http://localhost). Opening BoardRipper at http://192.168.x.x:1336, the Vite dev server's network URL, or a Tailscale 100.x.x.x address left navigator.clipboard undefined; both call sites threw Cannot read properties of undefined (reading 'writeText'). A new copyText() helper (src/frontend/src/clipboard.ts) tries the modern API first and falls back to a transient off-screen <textarea> + document.execCommand('copy') — works everywhere the dashboard is reachable.PartMeta (primarily TVW Teboview; partial coverage from BVR / BDV / Allegro), the in-canvas tooltip now shows a new line between R123 · pin 2 and the OBD readings, joining whatever fields are present with · — e.g. 10uF · CHIP0603R, 100K, or QFN32 on its own. The line is hidden entirely when both fields are empty, so boards without metadata keep the compact two-line tooltip. ComponentInfoPanel + BoardSidebar already showed these fields; the tooltip now matches without requiring a click. (07f018f)trace · Top (or just trace when the source format has no layer name), keeping the trace's net name on line 1 as before. The old phrasing was a leftover from constructing the detail string from a part/pin pair; pin became optional in showTooltip so trace hits can drop the suffix. (07f018f)Headline addition: the Worklist — a per-board, persistent multi-select with named lists, marks, notes, and roundtrip clipboard sync. Plus a stack of PDF / scanner / updater fixes from the day after the v0.20.x cleanup.
The version jump from v0.20.9 to v0.30.0 marks the first release of the new stable pipeline (signed manifest + GHCR + ripperdoc.de archive + chat-runnable release.sh) as a milestone, distinct from the 0.20.x stabilisation series.
boardripper-worklist) keyed off the same fileName/size/mtime triple as the board cache — they survive reloads and container upgrades. Each row carries a cycling mark state (none → replaced → reworked → cleaned → bandage) plus an optional free-form note under a spoiler caret. Per-mark coloured outline + glyph on the board so you can see at-a-glance which parts are in which state. The whole list copies as REFDES[mark] (note) for paste-into-issue tracking. The panel lives as a tab inside the BoardSidebar (no separate Dockview window). (2461b8c, 59bf9ff, df50276, 5cc5a02, 872b750, b64f143, f106dac)-[<name>-<bnum>]- marker, then REFDES[mark] (note) lines. Paste it back into another BoardRipper instance and the Worklist panel reads the marker, recreates the list, re-resolves refdeses against the open board, and restores marks + notes. importFromText is hardened against arbitrary clipboard payloads (random text doesn't accidentally create a list; only the marker-form is accepted). Survives renames + minor format drift in pasted content. (c962d30, 8dc817d)applyFlips() butterfly branch was actually already rotation-aware — it picks the separation axis from the rotated bounds and uses axesSwapped to flip the right board axis under 90°/270°. The toolbar gate was the only thing in the way. Preference persists per-install. (6e2b835)pdf.js v5's getTextContent() iterates the underlying ReadableStream with for await, which needs ReadableStream[@@asyncIterator] — absent on Safari before 17.4. Users on Safari 16.4–17.3 could render PDFs fine (the sendWithPromise path is unaffected) but PDF text scan / search returned nothing. The text-extractor now drives streamTextContent().getReader() directly. Safari stacks omit err.message, so log lines explicitly include err.name + err.message for diagnosability on the field. (609f8cc)preventDefault and triggering the browser-native rubber-band bounce, briefly showing the page behind the canvas. overscroll-behavior: none on html/body/#root and contain on .pdf-canvas-container close that. Additional polish: zoom-around-cursor honours transformOrigin consistently across resize transitions; ResizeObserver no longer fires spurious resizes during DPI changes. (a876c74)1.pdf matched every board whose name contained a "1" (i.e., most of them); a single page-fragment with a short generic name silently latched onto dozens of unrelated boards. Now a minimum substring length (≥4 chars) and a stop-list (pdf, boardview, page-marker patterns) gate the score. migrateV9 runs once on hydration to delete pre-existing garbage bindings produced by the old heuristic — the scan re-runs cleanly on next bind. (e81bdcd, 7c1ffce)updateStore.progress[] but were never rendered to the user. The overlay now subscribes to that array via a primitive progressLen useSyncExternalStore snapshot (keeps the stability invariant intact) and renders the last 14 entries in a scrollable monospace list, reusing the toolbar dropdown's existing .update-progress-line styling. New <Elapsed: Ns> counter ticks every second from overlay mount — the inner-component split means each restart transition mounts a fresh instance, so the counter naturally resets to 0 without a setState-in-effect. (8b87a16, 7f753d9 for the inner-component refactor cleaning up the react-hooks lint warning)0x0B drill-code is an ARC record, not another slot variant. ThinkPad P14s Gen 2 NM-D352 ships 59 of these 0x0B records alongside 90 real 0x0A slots in its OUTLINE Roul layer. The parser was folding 0x0A and 0x0B into the same DrillSlot branch (inherited from eagleview) and reading both as straight line segments — the misread center+radius bytes happened to plot as 4,000–12,000 mil diagonals back near the origin, and gfx.fill() then cross-hatched the board with PixiJS's even-odd rule across the 98 disjoint sub-paths. 0x0B has the same 29-byte footprint as 0x0A but a different field layout (net:s32, tool:u32, center:Vec2S, radius:Fixed32, start:f32 deg, sweep:f32 deg); the parser now tessellates it to a 16-segment polyline so chainLines sees connected geometry, matching the Logic-layer arc-tessellation constants. (0cc71c8)docker compose up -d no longer restart-loops on Linux hosts. Dockerfile:52 ships USER 65532:65532 for safety, but a fresh docker compose up on Linux has the Docker daemon create ./data as root, which UID 65532 can't write — databank.Open then log.Fatals at boot and the container exits. The bundled docker-compose.yml now overrides user: "0:0" (mirrors deploy-remote.sh) so the documented one-command install works. Users who'd rather keep 65532 can remove that line and either chown -R 65532:65532 ./data or switch to a named volume — Docker initializes named volumes from /data inside the image (pre-chowned to 65532 in the Dockerfile), so USER 65532 stays meaningful. (f456c12)SOURCES_CSV no longer lists GHCR as a manifest source. GHCR is a Docker Distribution v2 registry; https://ghcr.io/.../manifest.json returns HTTP 405 (it only speaks /v2/). Every install ever shipped wasted one HTTP request on a guaranteed-fail there before falling through to ripperdoc.de. Both the compiled-in SOURCES_CSV and the manifest's source_list_next field are now single-entry. GHCR is still used during Apply for pull-by-digest — different protocol on the same hostname. (c4666b0)scripts/release.sh is now fully non-interactive. Set MINISIGN_PASSWORD in ~/.config/boardripper/release.env and the script pipes it to minisign -S over stdin (minisign accepts stdin when not on a tty). Falls back to the interactive prompt if the env var is unset, so hands-on runs still work. Combined with the Build desktop Electron apps too? [y/N] prompt's existing tty-check (which falls through to "off" in non-tty contexts), a release now runs end-to-end with zero operator input — chat-driven, cron-driven, CI-driven all behave the same. --desktop flag forces Electron builds when needed. (fb0f91a)docs/RELEASE_ARCHIVE.md. (fb0f91a)user: "0:0" override matching the in-repo docker-compose.yml (was missing, copy-paste users hit the restart loop). Self-update section corrected — the signed manifest is fetched from ripperdoc.de, then the image is pulled by content-addressed digest from GHCR; the previous "GHCR primary, ripperdoc.de fallback" phrasing was backwards (GHCR can't serve manifests). A new "Older versions" subsection points at the archive page. (eca9d11)--max-warnings 100 dropped from the eslint step. The previous cap meant nearly every push hit the cap and failed CI (30 of the last 50 runs failed on the same react-hooks/preserve-manual-memoization and react-refresh/only-export-components warnings the React Compiler emits in batches); tsc -b --noEmit + backend tests + docker-build never ran because they needs: lint-and-typecheck. The cap was raised once before in 55a3a12 for the same reason — drift faster than the rules. Warnings still print to the run log; tsc --noEmit stays as the hard gate. (ae68bc7)/data is mixed-ownership. Since 430a219 (2026-05-12) the image ships USER 65532:65532. Production installs that override that at runtime (docker run --user 0:0, e.g. the maintainer's deploy-remote.sh:143 does this because the Synology bind-mounted data dir is mixed root/65532) ran the OLD container as root. The orchestrator's createBody, however, did not propagate Config.User into the NEW container — it always started at the image default 65532. The new binary then log.Fatal'd on databank.Open and update-secret read/create because the OLD container had written /data/databank.db and /data/.update-secret as root and 65532 can't write them; the process exited before listening on :8080; the orchestrator's 60s /api/health poll on the new container's IP timed out; rollback restored the old container; and the user saw nothing — the update "succeeded" silently and reverted. findSelfContainer now reads Config.User and orchestrateRestart includes "User": self.User in the create body. Image-default-USER installs (self.User == "") fall through unchanged. (b9b5e10) Chicken-and-egg caveat: the fix lives in v0.20.8's binary, but the orchestrator that creates v0.20.8's container runs v0.20.7's (or earlier) code — which still has the bug. So the v0.20.7 → v0.20.8 update on an affected install will still roll back. Recovery: one manual NASdeploy.sh / docker pull ghcr.io/alexeyinwerp/boardripper:v0.20.8 && docker rm -f boardripper && docker run -d ... boardripper:v0.20.8 to land v0.20.8 in place. From v0.20.8 onward, every future auto-update works as designed.
The update-test harness (tools/update-test/run.sh) now passes --user 0:0 on the OLD container, mirroring production. This reproduces the bug end-to-end (without the fix the harness fails the same way production fails); with the fix the harness passes in ~10 s. The new scripts/release.sh runs the harness as a mandatory gate before signing — same class of regression cannot land silently again.
Cannot set properties of undefined (setting 'fontSize'). The selected part's pin labels get raised into netLabelLayer mid-pass so they render above the netDim overlay. When pinNetLabelBg is on, those entries are Container wrappers (background Graphics + BitmapText child) rather than bare BitmapTexts. A later acquireNetLabel call in the same renderSelection walked netLabelPoolIdx into the wrapper's slot, cast it as BitmapText, and crashed on label.style.fontSize because Container has no .style. acquireNetLabel now skips past any non-BitmapText children at the current pool index before reusing or creating. Reported from a deployed v0.20.7 install. (248f8eb)release.sh's run, then release.sh committed counter 17, failed at git tag, and exited 128 with no rollback help) exposed that nothing enforced CHANGELOG-entry presence, tag-collision detection, type-check / build preflight, update-test before signing, push, or GitHub Release creation. scripts/release.sh now owns all of it. Interactive Build desktop Electron apps too? [y/N] prompt; --desktop / --no-desktop / --desktop-only bypass the prompt. After GHCR push the script runs tools/update-test/run.sh as a mandatory sanity gate before signing. After commit + tag it pushes main + tag and creates a GitHub Release with the sliced CHANGELOG section + (if built) Electron zips as assets. Pre-release tags (.beta / .rc / .alpha) get --prerelease instead of --latest. New --desktop-only mode bumps package.json + builds + ships Electron apps + creates a GH release without touching .release-counter, Docker, manifest signing, or FTP — the recovery path for "in-app updater is fine, but desktop users need a hotfix." See docs/RELEASE_RUNBOOK.md for the full flag cheatsheet. (4508c2d)chainLines would return dozens of 2-point sub-paths instead of one perimeter loop; gfx.fill() on that pile flowed PixiJS's even-odd rule across all the disconnected sub-paths and produced cross-hatched "wrong polygon fillings" (reported on ThinkPad P14s Gen 2 NM-D352). drawOutline now pre-passes to find the largest sub-path; when the largest is under 20 points (no real perimeter), it fills the outline-points bbox once and strokes the fragmented segments on top. Well-formed outlines (HY568, NM-D355 with its 415-pt perimeter) hit the existing path-fill branch unchanged. (1a40813)Check failed: HTTP 502 every 6 hours when you're already on the latest release. After a successful self-update the install's counter file is bumped to the applied manifest's counter. The next background check fetched the same manifest, ValidateManifest rejected it at the m.Counter <= installedCounter branch (replay defence), Check() returned an error, the handler answered 502, and the frontend dutifully logged red. Check() now distinguishes the "manifest is exactly what we already applied" case (m.Counter == installedCtr && m.Version == Version) from a real validation failure and reports it as no-update with HTTP 200. Replay / downgrade / counter-regression cases still bubble through to the user. (e184b94)inwerp/Boardviewer placeholder URL that 404s after the public flip). (350f636)cfb5f19)NASdeploy.sh bakes PUBKEY into the deployed image. The Dockerfile defaults ARG PUBKEY=""; release.sh always read ~/.config/boardripper/release.pub and passed it via --build-arg PUBKEY, but NASdeploy.sh only passed APP_VERSION. Every NAS deploy since the secure-update pipeline landed in v0.19.0 had therefore shipped with an empty PubKey baked into the binary, and every update check short-circuited at updater not configured: PubKey is empty. Users on a NAS deployed by NASdeploy.sh would see no update banner even with a fresh manifest live on GHCR + ripperdoc.de. NASdeploy.sh now reads release.pub (overridable via $PUBKEY_FILE) and passes it through. (11b0959)3267185), each panel got wrapped in a <div style={{ flex: 1, minHeight: 0 }}> that expected a flex parent — but .sidebar-content was a plain block container, so the wrapper fell back to content-sized height. LibraryPanel { height: 100% } then resolved against an auto-height parent and collapsed; the search row scrolled with the list instead of staying pinned at top, and the stats bar (with margin-top: auto) had no room to push to the bottom — it landed at the end of the file list. .sidebar-content is now display: flex; flex-direction: column so the active panel actually claims the sidebar's height, internal scrolling stays inside .library-content, and pinned top/bottom rows behave like they used to. SettingsPanel and DebugPanel had the same latent issue and benefit from the same fix.HistoryView, FolderView, LiveBrowser, and the filterFile callback synchronously on every character. On a small library that's invisible; on a multi-thousand-file library the main-thread work blocked the input event loop and typing felt laggy. The filter pipeline now debounces with a 200 ms trailing delay — the input itself binds to the raw value so typed text appears instantly, only the downstream filtering waits. Empty values short-circuit the delay so clearing via the "x" button stays immediate.data-testid="open-btn" (Playwright tests still pass on both builds), tooltip mirrors the distinction.server so per-user expansion state in localStorage carries over unchanged.h5/h6/h7 Pascal strings. Three pstr fields between date and size1 were being read as 3 raw bytes (const3 in eagleview). In every previously-known sample those fields are empty (3 zero length-bytes), so the raw read worked by coincidence. samples/BROKEN/NM-D355_r1.0_HT4BT.tvw ships one of them as "q798", which shifted every subsequent header field by 4 bytes; the parser then read layerCount = 2 instead of 20 and bailed on the first layer with "unknown object type 20." Fix: parse the three fields as Pascal strings (byte-compatible with all prior samples).u32 after each pin's name pstr was assumed to be Z2 == 0 (eagleview asserts this). It's actually a per-pin counter for opposite-side through-hole/edge contacts. When any pin in a part has it set, a mirrored-contact block follows the primary pin list: (u32 cont_flag, u32 reserved) then Σ ext_contact_count more pin records (no trailing counter of their own). The previous heuristic-based looksLikePinExtension detector was a weaker proxy for the same signal — it over-fired on NM-D355's H11 SWITCH (2 declared pins + 1 mirrored contact, read as 2 ext-pins instead of 1) and was inconsistent on Landrex connectors. Observed in three in-the-wild patterns: - LianBao SWITCH (NM-D355 H11): pinCount=2, sum=1 — mechanical switch with one mirrored mechanical contact
- Landrex vertical connector (Gigabyte GV-N5080 CN1): pinCount=82, sum=82 — every pin mirrored on opposite copper layer
- Landrex edge connector (Gigabyte GV-R79X MPCIE1): pinCount=82, sum=82 — PCIe slot dual-sided
NM-D355 0 → 3957 parts, GV-N5080 28 → 3161 parts, GV-R79X 0 → 4825 parts. Every other working sample unchanged. The format note is captured in docs/formats/TVW_FORMAT.md so upstream eagleview and future parsers don't make the same Z2 == 0 assumption.http.ServeFile was happy to stream whatever the kernel returned — sometimes a partial file or zero bytes — and the frontend parser would fail with "empty/truncated file." The two cloud-exposed file-serve handlers (files.Get, files.GetByPath) now route through a new serveFileEager (src/backend/handlers/serve.go) that reads the file fully into memory and verifies byte count matches stat().Size() before responding. Truncated reads return 503 + Retry-After: 5; the 30s read deadline returns 503 + Retry-After: 10. Frontend fetchWithCloudRetry retries up to 6 attempts / 3 min, surfaces a "Downloading from cloud storage…" toast on retry, and surfaces an error toast on exhaustion. databank.PreviewGet deliberately keeps http.ServeFile — previews live in the always-local <dataDir>/.previews/ and benefit from ServeFile's ETag/304 caching there.EDEADLK (resource deadlock avoided). serveFileEager now detects this specific error and returns 503 with a body that tells the user how to fix it: "Cloud-storage placeholder: file not yet materialized on host. Open it on the host (Finder → right-click → 'Keep on this device' for Google Drive/iCloud, equivalent for OneDrive) or sync your library to a fully-local directory." Native macOS reads of the same placeholder block 1–2 seconds for materialization and succeed normally — EDEADLK only fires inside the container, so the friendly error is scoped to the case where it's actually useful.ETag/If-Modified-Since caching are dropped on the two affected handlers. No current consumer relied on either (boardview parsers always read from byte 0; PDF.js doesn't issue range requests in the current implementation).serveFileEager. Boardview files are <10 MB, PDFs <100 MB; the cap is a safety net, not a hot path.<libraryDir>/.boardripper/openboarddata/. The library mount is :ro by default in Docker, so atomic writes silently failed and the index was effectively in-memory — gone on every container restart, including the restart triggered by self-update. Users had to re-sync (POST /api/obd/index/sync, ~2 min) after every release. Cache now lives at <dataDir>/obd/, which is the always-writable persistent volume by design. Existing caches at the legacy path are auto-migrated on first boot via obd.MigrateLegacyCache; cross-volume rename failures (typical when /library and /data are different mounts) fall back to a one-time re-sync. obdStore is no longer library-conditional — OBD works on fresh installs that haven't configured a library yet.viewMode === 'folders' && browseMode === 'live'). The filter wired to the database-backed FolderView was silently dropped on the LiveBrowser side. LiveBrowser now receives searchFilter from the same localSearch state every other view uses, applies a case-insensitive substring filter to both directories and files in the current directory. A directory whose name matches stays visible so the user can navigate toward something they remember the parent name of. No descent into subdirectories — that stays as future work.<component> · pin <pinId> · net <netName> in board mode, or the cursor word in PDF mode. Hidden when the relevant fields are empty. Removes the "wait, what am I about to copy?" pause that the new icon-strip introduced in v0.20.1.pdfStore.countTextMatches walked every line of every page synchronously, called 3× per PDF donor (default query, chip@pin variant, net variant), all before the menu rendered. With one bound PDF that's 3 sync scans gating the open. New countTextMatchesAsync yields every 8 pages via setTimeout(0); ContextMenu dispatches the counts in a useEffect after first paint and replaces (…) placeholders as each promise resolves. AbortController cancels stale work when the menu closes or the selection changes. Board counts (countInBoardTab) stay sync — they walk in-memory parsed objects, not text.~ library toggle (with the layout note about Backquote / IntlBackslash and the AZERTY caveat), and the configurable Settings ▸ Navigation knob.~ toggles Library sidebar). Pan and zoom work on both board and PDF panels; rotate is board-only and silently no-ops on PDF. Library toggle binds to the physical key left of 1 via KeyboardEvent.code ('Backquote' on US, 'IntlBackslash' on German Mac), so ~ on US, ° on German DE both fire the same toggle. Existing Cmd+arrow / Space / Cmd+F shortcuts unchanged. Auto-rendered in Settings ▸ Shortcuts and the home-screen Getting Started card under a new "WSAD Navigation" section. Pan and zoom step sizes configurable in Settings ▸ Navigation ▸ Keyboard pan / zoom (default: 10% of screen per pan, ×1.32 per zoom press; previous defaults were 15% / ×1.72).navigator.clipboard.writeText with toast feedback; Search opens Google in a new tab with noopener,noreferrer. Strip hides entirely when no entity is selected. The existing donor-row search functionality is unchanged below.code (KeyboardEvent.code, single string or array for multi-layout binding), displayLabel (formatter override), and ignoreShift (matcher accepts shift-held events when set, used by the ~ library toggle so Shift+Backquote fires the same as bare Backquote). Foundation for layout-aware keybinding (AZERTY remapping is queued for a future release).if (!requireShift && e.shiftKey) return false; was missing — bare W would match Shift+W events, blocking the new zoom shortcut from registering distinctly. Added the symmetric guard. The Shift+Cmd/Ctrl+F "previous match" path (documented in the focusSearch shortcut description) re-enters with { ...focusSearch, shift: true } so the routing block still receives both directions./v1.44/ to fix Docker Engine 29.x rejecting /v1.41/ as "too old"; the same hardcode now broke Synology DSM (Docker 20.10.3, max API 1.41) which rejects /v1.44/ as client version 1.44 is too new. Maximum supported API version is 1.41. Every code path through docker.sock — pull, load, inspect, orchestrate — hit the same 400. Bundle-drop also affected (shares dockerLoad). User-reported on the maintainer's NAS, manual SSH swap was the only escape until this release. Fix: probe GET /_ping once on first use, parse the daemon's Api-Version header, and prefix every Engine API call with that. sync.Once-gated; falls back to v1.41 if /_ping itself fails (in which case nothing else would work either, but no panic at first use). Replaces all 13 hardcoded http://docker/v1.44/... strings; the orchestrator shell script's API= line gets the same value via Sprintf so the alpine-curl payload speaks the same dialect. Forward-compatible to whatever Docker bumps the floor to next.buildRenderedBoard's filtered branch (BOM-alternate filter / hide-ghosts toggle) returned nets: rev.nets from before any parts were dropped. Net.pinIndices are positional partIndex refs into the array they were built against — after dropping a single part every index past that slot pointed one element off in the filtered array, scrambling every net on the board. Latent since 176cced (2026-04-14, hide-ghosts toggle, default off — almost no one hit it). Default-on as of 48ce8ae (2026-05-05): BOM-alternate cluster filtering with showBomAlternates: false default means any CAD/TVW file with ≥1 detected cluster drops one part on first render. ROG STRIX RTX 4090 sample (1 cluster, C1903/C1906) was the user-reported canary — clicking PC101 lit PC104 / PC258 / C415 instead of PC265, and 12V_F_R1's 36 refs landed on a completely different 27 parts than the file actually says. Fix: rebuild nets via buildNets(filteredParts) whenever any part is dropped. Regression spec asserts every pinIndex in every net resolves to a pin actually on that net after filtering./v1.41/; Engine 29 rejects that with HTTP 400 (client version 1.41 is too old. Minimum supported API version is 1.44). The first call (findSelfContainer) parsed the {"message":...} error body as a []struct and failed with cannot unmarshal object into ... []struct; tagPrevious warned and continued, but every later docker.sock call also 400'd and the orchestrator was never created — Apply silently bailed out part-way. Bumped all Engine API URLs to /v1.44/ (single dockerAPIVersion constant; keeps Docker 25+ compatibility — the floor on current Synology DSM Container Manager). findSelfContainer now also checks resp.StatusCode and surfaces the error body so the next floor bump shows up as Docker API HTTP 400: ... instead of an unmarshal error.go h.upd.Apply() discarded the return value, and several pre-orchestrate failure paths (manifest sha mismatch, findSelfContainer failure, etc.) returned without first logging a status: error progress entry. The frontend's SSE stream would just go silent and the UI hung on "Updating…" until the 2-minute health-poll timeout. New Updater.PushError helper funnels the return value into the SSE channel as a terminal error entry; ApplyBundle got the same treatment.waitForRestart reloaded the page against the about-to-die backend. Apply() returns the moment the orchestrator container is *launched*, but that orchestrator's first action is apk add curl (5–15 s) before it stops the running container. During that window /api/health on the OLD container returned 200 happily — the previous loop accepted that as "swap done" and reloaded the page right back into the dying backend. The user then saw a "rolled back" log line and sat on the unchanged UI until the next 30-minute background poll. Now waits for an authoritative /api/update/status to report the manifest's expected new version (cookie survives the swap because the per-install secret persists in /data); falls back to a downtime-then-up /api/health sequence if status auth races. Reload only fires when the swap is real.wheelDetection burst-latch in scroll-mode.ts was defaulting every new burst to "wheel" — slow trackpad scrolls (gaps >35 ms — never reach the 6-fast-cadence demotion threshold) stayed latched and got reinterpreted as zoom. The first event of a new burst is now classified by signature (large integer deltaY with no deltaX → wheel; ctrlKey, deltaX, small magnitude, or fractional → trackpad), preserving the original Mac smooth-scroll mid-burst-flip fix while fixing slow trackpad scrolls. (2) The PDF panel's wheel handler no longer consults looksLikeMouseWheel at all — the safety net's purpose ("classic mouse wheel + pan mode = unusable") is board-only; on PDF, bare wheel scrolling is the natural way to walk through pages even with a real mouse wheel. Pinch (ctrlKey) still routes to zoom.tools/update-test/). Builds OLD + NEW docker images pinned to a throwaway minisign key + a local Python http.server mirror at host.docker.internal:18000, signs a mock release manifest, starts the OLD container with /var/run/docker.sock mounted, and drives Playwright headless Chromium through the full apply→swap→reload, screenshotting every 2.5 s into results/. Asserts overlay visibility during restart, version flip via /api/update/status, and absence of JS crashes during the disconnect window. The harness reads __brUpdateStore.{updating,restarting,progress.length} directly via a deliberate window export so it can probe internal state without walking the React fiber. One command — cd tools/update-test && ./run.sh (add BR_HARNESS_HEADED=1 to watch). Cached re-run completes in ~1 minute. Run before any future updater change.eventMode from their parent — the viewport is interactive (so pins receive clicks), so every Graphics added underneath was *also* interactive and any painted pixel under the cursor counted as a hit. The full-board dim layer drawn in those modes silently swallowed clicks before they reached the pin sprites; tiny 0402 caps and dense BGA pins were the worst affected. Decoration layers now explicitly set eventMode='none' at construction *and* at every renderSelection (the latter is an HMR safety-net — Vite replaces the module without re-instantiating the PixiJS Application, so layers in a hot-reloaded session would otherwise stay stale on 'auto'). User-reported as "extreme regression."findFoldAxis(). Now early-returns when the outline decomposes into ≥4 connected components that all pair off by (width, height, segCount); per-board X-fold axes from boardGroups are the only thing the UI applies, with manual per-board folding still available from the sidebar. PARSER_VERSION bumped to invalidate cached entries.applyFlips, then pans so the same world point lands at screen-centre again after the rotation completes. Net-line geometry is now also recomputed and redrawn immediately on rotation (previously stayed at pre-rotation world positions until the next selection/pan/pulse-tick).rotateFlipAxis() now flips the stored 'x'|'y' whenever rotation crosses an axes-swap boundary so the screen direction the user selected is preserved. Toolbar icon and tooltip ('⇅ Vertical' vs '⇄ Horizontal') reflect the actual screen-axis result.landing/, excluded from the Docker image (the Dockerfile only COPYs src/frontend/, src/backend/, and Board Database/boards.db). See landing/README.md for the update workflow.Four findings from the 2026-05-07 review report; all sub-millisecond individually, compounding under sustained interaction. The bundle was reverted once when its eventMode interaction with the dim-layer bug surfaced as a click-blocking regression, then re-introduced piece by piece after that bug was traced to a separate root cause and fixed independently.
Map<color, Segment[]> and {start,end} wrapper allocations (added in a9d99b4 for chain-adjacent) are now built once in recomputeNetLineSegments (already dirty-tracked) as netLineSegmentsByColor. ~30 allocs/frame → 0 (single net) or ~150 → 0 (chain-adjacent on a 60-net rail).[...adjacentNets].sort().join(',') sentinel with .size compare in lastRenderedSel. Content changes always co-occur with a change in (partIndex, pinIndex, highlightedNet, board) — the BFS inputs — so size is a sufficient sentinel. Saves ~0.05–0.3 ms per store notify on a 60-adjacent rail; biggest win during search iteration / PDF-binding refresh.pointermove path. formatObdForNet was running 6 regex tests + O(matches × |nets|) per move (~4 500 string compares on a 3-variant × 1 500-net board). Now: obdNetIndex(boardNumber) exposes a snapshot-keyed Map<netName, ObdNet[]> cached on a WeakMap keyed by the obd-store snapshot, and BoardRenderer memoises extractBoardNumberFromFilename against boardStore.fileName. Per-move cost: 1 string compare + 1 Map.get.crossSideGhostParts from number[] to Set<number>. Two .includes() calls in the per-pin chain-mode net-line builder were O(g) linear scans called for every pin reference of every active net (~30 000 array scans → ~600 hash lookups on a busy 5 V rail with 60 nets, 10 pins, 50 ghosts).Closes the two Important findings from docs/analysis/2026-05-07-updater-security.md. The crypto primitives were already well-covered; these tighten the surrounding I/O envelope.
released_at freshness window in ValidateManifest. The existing check rejected expired manifests (90-day not_after) but ignored released_at, so a compromised mirror could re-serve any signed-but-stale manifest from anywhere in the 90-day window — defeating the counter check on first install (where installedCounter == 0 skips), and freezing installed clients on outdated releases. Now requires released_at ∈ [now − 30 d, now + 24 h]. The 30 d past bound is wide enough not to bite the maintainer's normal cadence (5 releases in 9 days during the v0.19 cycle); the 24 h future slack tolerates clock-skew between signing host and client. Manifests without released_at are rejected outright.downloadAsset() previously did a plain http.Get() with no timeout, no size cap, and no streaming integrity check; applyTarball then re-read the whole tarball off disk to compute SHA-256, doubling peak RAM. New downloadAssetVerified() does it in one streaming pass: 10 min http.Client.Timeout, body cap = manifest's signed SizeBytes (or 1 GiB legacy fallback) by reading one byte past the cap so over-long streams are observed not silently truncated, incremental SHA-256 via io.MultiWriter(file, sha256.New()) so peak memory stays at io.Copy's 32 KiB buffer. Rejects on size mismatch, SHA mismatch, or non-200.parseDockerImageRef and selectNewImageRef extracted as pure functions and covered with 25 tests across parseDockerImageRef (6 forms incl. embedded-colon-in-digest), selectNewImageRef (4 paths incl. the v0.19.2 fallback case), extractBundle (path-traversal guard, bsdtar/gnu ./ parity, ignored-extras), bindsFromMounts, and shortID.release.sh was reading --raw | jq '.manifests[0].digest' for the BoardRipper image, which grabs the *first* platform manifest (amd64) from the multi-arch index, then signing that amd64-only digest into manifest.json. amd64 hosts pulled fine; an arm64 install would error. Now uses the non-raw imagetools inspect's top-level Digest: line, hard-failing if the parse returns empty. (b) Same bug class on alpine:3.19 for the orchestrator: docker pull --platform linux/amd64 then RepoDigests[0] gave the per-platform manifest digest, not the index digest. Now pulls without --platform and reads via buildx imagetools. v0.19.5's NAS deploy was unaffected because the maintainer's NAS is amd64-only.When the user clicks Update Now, BoardRipper now shows a centered modal: *"Update in progress — the page will reload automatically in 30–60 seconds."* The modal stays up across the SSE-disconnect window (the orchestrator deliberately stops the running container, killing the progress stream — that is the expected success path, not a failure). Once the new container's /api/health responds, the page reloads automatically; the modal vanishes.
A boardripper-update-in-flight flag in localStorage persists across page refreshes mid-update — refreshing the tab while the update is in flight no longer presents a fresh dashboard with an "Update" button that could be clicked again. The flag is cleared on completion or after 5 minutes (whichever comes first). Backend health-poll runs every 2 seconds for up to 120 seconds while waiting for the new container.
When the in-app update button can't reach GHCR or ripperdoc.de — or when a future broken-orchestrator bug strands an install — users can now download a single bundle file and drag it onto the BoardRipper window to apply the update. Each release publishes boardripper-update-vX.Y.Z.tar (and a stable latest-update.tar alias) at https://www.ripperdoc.de/boardripper/releases/. The bundle contains the signed manifest, its signature, and the OCI image tarball; the running container verifies the signature against its compiled-in public key, validates counter/expiry/min-version, checks the tarball sha256, then runs the same orchestrator restart as the network path. Same trust envelope: only the manifest signature grants trust; the file itself is untrusted bytes until verification passes. Recovery escape-hatch for any future broken-self-update situation, but only available once the running container is on v0.19.5+.
update-store.ts gains restarting / restartingFromVersion getters and an internal streamProgress() + waitForRestart() flow shared between apply() and applyBundle().UpdateProgressOverlay React component, mounted at the App root, gated on updateStore.restarting.POST /api/update/apply-bundle (multipart upload, same auth-cookie middleware as the other /api/update/* routes).updater.ApplyBundle([]byte) reuses every existing piece (VerifyManifest, ValidateManifest, VerifyTarballSHA256, dockerLoad, orchestrateRestart).release.sh now produces out/boardripper-update-$VERSION.tar alongside the regular tarball and uploads it to FTP atomically.scripts/release/site-artifacts.sh no longer requires pandoc — built-in renderer (perl + sed + awk) handles the BoardRipper CHANGELOG.md format. Without this, missing pandoc on the maintainer's machine silently uploaded a 141-byte stub instead of the rendered changelog.http://<container-name>:8080/api/health, but Docker's default bridge network does not provide DNS-by-name for containers (only user-defined networks do). The poll never resolved, the 60-second healthcheck timed out, and the orchestrator restored the previous container — looking from outside as if "the update silently undid itself." Fix: query the new container's IP via containers/{id}/json and poll that IP. Falls back to name lookup if IP can't be parsed (preserves user-defined-network behavior).0.19.0 while backend was on 0.19.2). The frontend bundle injects __APP_VERSION__ from src/frontend/package.json at build time, which was being bumped by hand. The backend version comes from release.sh's --build-arg APP_VERSION. The two drift apart whenever release.sh runs without a prior package.json edit. Fix: release.sh now writes $VERSION (sans v prefix) into package.json before the build, then commits the change as part of the release commit. Single source of truth from this release on.Existing v0.19.0–v0.19.3 installs cannot auto-update to v0.19.4 (their bundled orchestrator still has the healthcheck-by-name bug). One manual docker pull ghcr.io/alexeyinwerp/boardripper:v0.19.4 && recreate-container is required. After landing on v0.19.4 once, future auto-updates work normally.
crossSideGhostGfx Graphics object every tick (clearing, recomputing each part's polygon/bounds, drawing fill+stroke+pins), running at 60 fps regardless of whether the user was interacting. On boards with many hidden-side parts on a selected net, this competed with viewport updates and produced visible stutter. On top of that, onZoomFrame() was clearing the ghost geometry on every zoom frame, so during continuous wheel scrolling the ghosts vanished and reappeared on each 32 ms settle, producing the visible "flash".'moved' event; pan and zoom no longer pay the per-frame Graphics rebuild. Phase doesn't advance during the pause, so the breathing resumes jump-free once the viewport settles.onZoomFrame() — ghost stroke widths are world-space and stay visually correct at any zoom, so the ghost stays drawn (frozen at last alpha) during zoom instead of vanishing/reappearing.boardripper:<version> (a leftover from the legacy tarball-load deploy convention), but the GHCR pull stores the image as <registry>@<digest> with no local named tag. The Docker daemon returned 404 on containers/create, the orchestrator's set -e killed the script before the rollback path could run, and the old container was left renamed to -old and stopped.<registry>@<digest> reference, falling back to <registry>:<tag> if the digest is absent. Both pull-by-digest and tarball-load paths resolve correctly.docker pull ghcr.io/alexeyinwerp/boardripper:v0.19.2 && recreate-container is required. After landing on v0.19.2 once, future auto-updates work.docker rename boardripper-old boardripper && docker start boardripper puts the host back on the old version.First release through the new pipeline end-to-end (no GitHub Actions). Pure cosmetic fixes; the update flow itself is what's being validated.
Cmd+Scroll / Win+Scroll form for the PDF meta slot, while the Settings panel said Shift + Scroll / Ctrl + Scroll (fast) and ⌘ + Scroll / Ctrl + Scroll. Both surfaces now use the same wording.Left-drag, Shift + Left-drag, Scroll, Shift + Scroll / Ctrl + Scroll (fast), ⌘ + Scroll (Mac) / Ctrl + Scroll (Windows/Linux).Board: CLICK+DRAG → Board: Drag; Board: 2Finger/Scroll → Board: Scroll; PDF: Scroll consistent.scripts/release.sh no longer uses the unsupported lftp mv -f syntax — atomic rename now does explicit rm -f && mv. (Already fixed in 84308b3; this is the first release that benefits.)Updates no longer require GITHUB_TOKEN. Each release is now signed offline by
the maintainer (Ed25519 / minisign), and the running container verifies that
signature against a public key compiled into its own binary before applying any
update.
For end users: you can remove GITHUB_TOKEN from your docker-compose.yml
after this update. The toolbar update button keeps working with no token. If you
prefer to update manually, both sources are public and free:
docker pull ghcr.io/alexeyinwerp/boardripper:latest docker compose up -d
…or the signed-tarball mirror (no Docker registry required, useful behind
firewalls):
curl -O https://www.ripperdoc.de/boardripper/releases/latest.tar.gz docker load < latest.tar.gz docker compose up -d
What changed under the hood:
ghcr.io/alexeyinwerp/boardripper (public registry, fast layer dedup) and https://www.ripperdoc.de/boardripper/ (signed tarball
mirror). Updater walks them in order and accepts the first source whose
manifest signature verifies. A hijacked mirror cannot deliver a forged update.
manifest.json carries version, counter, released_at, not_after, important flag, image digest, and tarball
sha256. Replay/freeze attacks closed by a monotonic counter; dropped manifests
closed by a 90-day expiry.
installs without you clicking. Releases marked important (security fixes)
show with a red banner instead of the normal blue.
within 60 s of starting, the orchestrator auto-reverts to the previous image.
/api/update/*.** A 32-byte secret is generated on first boot (/data/.update-secret, mode 0600). LAN drive-by requests to
/api/update/apply now return 401. The web UI bootstraps an `HttpOnly +
SameSite=Strict` cookie on first load.
Maintainer release runbook: docs/RELEASE_RUNBOOK.md.
Promise.withResolvers for older browser engines (R3dfox / Mypalon Win7 etc.). pdfjs-dist@5 calls it directly and would throw before any PDF
byte was read.
CLAUDE.md documents the landing/ folder workflow.This release is the last one published to the private GitHub Releases page —
it's the bridge release that moves existing token-using clients onto the new
system. From v0.19.1 onward, releases will only appear at GHCR + ripperdoc.de.
---
DecompressionStream with pako.inflate for tighter error reporting. Combined fix: ~80% of previously-broken FZ files now load.Themes are now two independent surfaces. The THEMES registry covers board-side concerns only (pin colours, part fills, background-of-board) and the board adopts whichever entry matches its file family. UI chrome obeys three independent knobs the user can set from the QuickSettings home dashboard or from Settings ▸ Themes:
accent — buttons, focus rings, primary chrome (with auto-flipped text colour against perceived brightness)background — app shell backgroundchrome — toolbar / status bar / sidebar chromeFive accent presets ship: BoardRipper default (recoloured away from generic AI-cliché blue), and four ATARI homages (Pantone Bright Red C plus the Atari 2600 silver-label rainbow stripes). Each knob persists separately.
11th supported format. Mentor Graphics Boardstation/Expedition exports a plain-text "neutral file" with the .cad extension shipped with some Samsung / Quanta / Compal / Acer notebook board packages — not GenCAD despite the shared extension. Detection cue: # file : ... first comment + BOARD ... OFFSET ... ORIENTATION record + ###Section banners. Outline is synthesized from drill-hole geometry. See docs/formats/MENTOR_NEUTRAL_FORMAT.md for the full spec; AGPL provenance recorded in the spec footer.
The floating in-canvas overlay (top/bottom toggle, flip-axis, parts/nets filters, dim-mode tri-state, selection-name label) is now slot-driven and user-customizable. Drag-and-drop in Settings ▸ Board overlay reorders or hides slots; "Add separator" inserts a divider; layout is persisted. Parts and Nets dropdowns use a shared popover scaffold with a memoized natural-sort index (refdes-aware) and a No-Connect partition for nets.
The dim-mode button cycles three states (off / search-dim / spotlight) — spotlight is a smooth dark gradient with a clear core sized to the selected component; selected pins draw above the spotlight so the component stays fully bright.
The HomeBackdrop dashboard now carries a Bindings matrix (board↔PDF associations from the library) and Behaviour toggles (auto-open PDF on board load, theme switch). The QuickSettings strip got a compact accent picker.
useThemeOverrides useSyncExternalStore infinite loop. Snapshot now caches a stable reference; the same fix shape applied to HomeBackdrop earlier in the cycle (01eda1c).?? DEFAULTS + try/catch defensive paths in fresh code).theme-store.ts consolidated; the parallel registry shipped as a stop-gap was dropped.boardOverlay slot registry under components/BoardOverlay/ with per-slot toggle components and a Separator slot.panToPart / panToNetIfOffscreen helpers added in renderer; focus-zoom capped at 3× fit-to-board scale.CullerPlugin enabled. Off-viewport pin labels and parts no longer pay GPU per frame; expect 5–20× p95 improvement at deep zoom on dense boards. Closes a long-deferred research item.A second Allegro parser family. v15.x (magic 0x0012XXXX) is a different binary from the v16/v17 family already supported (0x0013XXXX) — different header, different block table — but many block payloads are shared. Reverse-engineered blind from a 15.5.7 / 15.5.2 corpus over the previous week:
Spec: docs/formats/ALLEGRO_V15_FORMAT.md. Future-work items captured inline.
BRDOUT: 0 0 0 (zero outline) regression. v0.17.0-development restored the max-part-Y mirror axis fallback for files that ship a zeroed BRDOUT (e.g. creator 1457685 / DAG3BEMBCD0 — HP 17-an100 Quanta G3BE). Canary regression test pins it..gitkeep-shaped markers) was bypassing the local-cache "skip if same size" check because zero-size compared as falsy in the diff path; we now treat 0 as a real size.<D:href>/Library/Apple iPhone 14/...</D:href> were splitting on the space; sync skipped any board folder whose name contained a space. Fix: parse <D:href> as a single token, URL-decode after extraction.A scheduled background sync pulls a remote WebDAV-served library mirror into the local /library/ mount. Settings ▸ Library exposes the endpoint, schedule, and a "Sync now" button. Diff-then-fetch semantics; per-file resume; never deletes remotely-missing files (Phase 1: pull-only). Useful for repair shops who keep an authoritative library on a NAS or office server and want every workstation to mirror it without manual copy.
partType=0x11 too — fixes broken pin geometry on the Gigabyte/Landrex variant.BoardRipper now reads the public OpenBoardData corpus — community-maintained per-board diagnostics, pin readings, schematics, and notes — and surfaces it inline.
OBDATA_V002 parser, filesystem cache with atomic writes + bpath sandboxing, scraper with drop-guard, four HTTP handlers under /api/obd/* with single-flight, integration tests.obdStore, useObdForBoard hook, Settings ▸ Library tab with disclaimer + "Sync OBD" button, ObdSection in LibraryPanel detail with a multi-variant table and visible comments. Canvas tooltip + Info pane surface readings on hover.The aligned-with-real-format scraper fix in this release brought OBD live.
Unsorted. Round 1 imported 1024 new boards.MacBook Pro is one family, not three near-duplicates separated by capitalization)..cae and .xzz (the parser handles .xzz; the scanner doesn't).--search-backend tavily). LLM classifier is now searchable in three modes: offline (heuristic only), DuckDuckGo, Tavily.apple-boards.ts retired — the hardcoded Apple-board lookup is gone; boards.db is now the single source of truth.FOREIGN KEY constraint failed (787) now bounded — previously one full line per failed pair hammered stdout and the writer mutex on a busy rescan after Reset All.boards.db is now bundled inside the Docker image at /build/boards.db. Fresh installs no longer need a side-channel DB download.The board reference database expanded from 145 hand-curated entries to 2,914 via three import slices, all converging on the v2 schema:
1. Wikidata Macs import (Slice 1). SPARQL fetch of all Apple Mac models → staging file → apply with INSERT OR IGNORE under v2 placeholders. Family resolver auto-assigns MacBook / iMac / Mac mini / Mac Pro / Mac Studio.
2. XZZ Apple-laptop skeleton import. Replaces the Wikidata path with a filesystem walk of the maintainer's XZZ corpus — recovers boards Wikidata doesn't carry (Quanta / Compal / Foxconn ODM codes).
3. Filename-scan importer (Slice 1). A pattern battery walks /library/, cross-references existing boards, tokenizes unmatched substrings, and emits a Markdown observation report + JSON sidecar. The JSON sidecar feeds an importer that creates placeholder Brand/Family/Board entries with INSERT OR IGNORE. 2.8K new boards added in one pass.
A snapshot of the 2026-04-29 observation report is archived under docs/scan/archive/.
feat/wikidata-macs-import, feat/filename-scan-observation).The flat boards table is replaced by an entity hierarchy: Brand → Family → Board, with a color cascade and an explicit family field on each Board. The v2 resolver walks the hierarchy and returns the most specific colour/identity available; UUIDs are always freshly generated in the migration so old BoardColorHex values don't pin to retired entries.
FAMILY_PATTERNS extended.boards.db rewritten on the v2 schema; create_mockup_db.sql rewritten; build_full_db.sql archived.---
For releases prior to v0.15.0, see the git tags directly:
(maintainer-only access until the repo is open-sourced).
BoardRipper — project page