# BoardRipper changelog ## v0.52.0 — 2026-11-11 ### Added - **Desktop app updates itself.** The desktop app checks for a new version at start, every few hours and from *Check for Updates…* in the app menu. It downloads the new build, checks it against a feed signed with the release key, and swaps it in when you quit or press *Restart Now*. This release is the first one with the updater, so it has to be installed by hand once; later releases arrive by themselves. Measured on macOS; Windows and the legacy macOS build are not yet tested. ([#42](https://github.com/AlexeyInwerp/BoardRipper/issues/42)) - **Worklists are stored on the server.** Each worklist is its own document on the BoardRipper server, or in the desktop app's own folder, instead of being tied to one browser and one board file. The same worklist can be open on two boards at once, and an edit made while the server is unreachable is kept and sent later. Existing worklists are copied over the first time the app opens in a browser that has them. - **Worklists have a board name and tags.** Every worklist is attributed to a board name, such as `820-02016`, and can carry tags, such as `A2337` or `water`. Both are edited in Tools ▸ Worklists, which now lists every worklist grouped by board name, with search, a tag filter and the rows themselves editable without a board open. - **Matching worklists are suggested.** The board's Worklist tab shows how many worklists match this board by board number, file name, model or tag, with *Open* on each and *+ New*. Nothing is loaded automatically, and loading one on a board lasts for that session only. - **MCP: `worklist_list` and `worklist_read`.** An agent can read every stored worklist without a browser open. - **Minimap header.** A strip above the minimap holds ‹ › for history Back and Forward, a switch between *History* and *Worklist*, and a button that opens the matching tab. Drag the header to move the map. On a touch screen the header buttons are finger-sized. - **History on the minimap.** The parts you visited on this board in this session are marked on the map. The place Back has taken you to is the strongest; older places fade, and the places Forward would return to show as outlines. *Show history* and *Steps* in the map's right-click menu switch it off or set how many places it marks. - **Worklist on the minimap.** In *Worklist* mode the map marks the loaded worklist's parts in their mark colours, and ‹ › step through its rows, as the worklist's own ‹ › do. - **Minimap placement, size and opacity.** Right-click the map to put it left, centre or right, or let it float where you click. *Size* and *Opacity* sliders sit in the same menu. The opacity fades only the board picture, so the marks keep their contrast, and the map comes to full strength while you pan or drag it. ### Changed - **The minimap is on by default**, centred at the top at 60 % opacity. Installs still on the old defaults (shown while panning, at full opacity) are moved to the new ones once. A minimap switched off stays off. - **The PDF page scrubber is off by default.** Switch it on in Settings ▸ PDF Viewer ▸ Navigation. ### Fixed - **Top and Bottom badges in the board Search tab** were dark blocks on the light colour schemes. They now follow the scheme and line up in one column. - **The minimap's board picture lost its right and bottom edge pixel**, and on a tall or rotated board the map was too narrow for its controls. ## v0.51.0 — 2026-11-09 Several of the board changes in this release come from **Roger Barbara and Rogério Pires of 2R Tools ([@reparomac](https://github.com/reparomac))**, who ship a customised BoardRipper inside their Scanner Diagnostic Suite and published their changes under the AGPL in [reparomac/boardripper-2r](https://github.com/reparomac/boardripper-2r). The ideas below marked *from 2R Tools* were reviewed from that fork and re-implemented here. Thank you, Roger, for the requests behind them and for giving the work back. ### Added - **Loose name search.** The ribbon's *Find part* and *Find net*, the board sidebar's Search tab, the board highlight and the global search's board count ignore case and the separators `_ - . /`, and accept several words in any order: `pp_1v25`, `1v25 nand` and `1v25nand` all find `PP1V25_AWAKE_NAND`. Exact matches keep their place; names only the loose rule finds follow under *Similar names*. *From 2R Tools.* - **N / Shift+N steps through results.** On a lit net, N visits each part on it, at its pin on the net, parts with up to three pins first — the easy probe points. Otherwise it steps through the parts the search found. On a PDF it steps through the matches. *From 2R Tools.* - **Pin table filter and sort.** Parts with more than 12 pins get a filter field above the pin table in the Info tab (pin number, name or net). Clicking *#*, *Name* or *Net* sorts the column; again reverses it; a third time restores file order. *From 2R Tools.* - **Worklist ‹ ›.** The worklist header steps through its rows in order, parts then nets: each step selects the row and takes the board there, and a net row opens its reading fields. - **Legend (?).** A new ribbon button opens a half-transparent card that says what the board is showing, in five modes: pin colours with this board's pin count per rule (including the colour no rule claims, per side), part type colours, diode reading colours, net line and selection colours, and the board keys. Drag it by its header; double-click the header to put it back. The pin colour legend idea is *from 2R Tools*. - **Minimap: always on.** The minimap button now cycles off → while panning → always on; the always state is the map with a pin. ### Changed - **Flip axis is back between Top and Bottom.** The one-click ⇅/⇄ toggle the toolbar had before v0.39 sits inside the Side switch again. The ⋯ menu keeps the same choice. Switch it off from the ribbon's right-click menu. - **Diode readings next to pin numbers and net names (supplement mode)** get their own place on IC and BGA pins: the reading sits in the pin centre on a plate, with the pin number above and the net name below, and the three share the pin spacing. A reading appears once it is as legible as a net name, instead of as a smudge over its neighbours. Diode-only mode is unchanged. The declutter idea is *from 2R Tools*. ### Fixed - **Desktop app: the MCP backend listens on this machine only.** With MCP turned on, the desktop app's built-in server listened on every network interface, so other machines on the network could reach the library and file endpoints. It now binds to 127.0.0.1. A new `BIND_HOST` setting does the same for any server install; unset keeps today's behaviour. *From 2R Tools.* - **MCP board actions report failure honestly.** `select_part` and `highlight_net` said they had succeeded for a part or net that does not exist, or a selection that did not take. Contributed by **[@pigeon-labs-stack](https://github.com/pigeon-labs-stack)** in [#44](https://github.com/AlexeyInwerp/BoardRipper/pull/44). - **The PDF text index finishes on large libraries, and search answers in seconds.** A file that crashed or timed out the extractor was retried at every start; it is now given up after three attempts, a long document cut off part-way keeps the pages read, and Stop interrupts the file in progress. Search reads the index in bounded chunks and says when results are partial. The Library tells a failed or cut-off search from "no results" and offers Retry. - **NAS installs: a fragmented index file is compacted.** On btrfs (the Synology default) the index file was shredded by copy-on-write until every query ran past its deadline. New index files are created without copy-on-write, and Settings ▸ Library ▸ *Compact index file* rewrites an existing one in place, keeping every extracted page. - **Index stats no longer take minutes** on a large index; the Library polls them during a sweep. ## v0.50.2 — 2026-10-23 ### Added - **Library files in the global search.** The toolbar search lists the first 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. - **Find in this PDF.** The PDF's right-click menu has a magnifier next to copy and web search that searches the open PDF for the text under the cursor. ### Changed - **⌘F / Ctrl+F on a board lands in the ribbon's find field** — *Find part* 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. - **⌘F on a PDF keeps the query.** A word clicked since the last ⌘F becomes 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. - **The browser's own shortcuts and menu stay out of the way**: ⌘F, ⌘G, ⌘S, ⌘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). ### Fixed - **Popups at a scaled interface.** With the interface scale at anything but 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. - **Panning no longer stops at the panel edge.** A mouse drag follows the 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. - **⌘F after a click in a linked schematic stays in the schematic.** The 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 first PDF opened now has a working ⌘F.** A panel that was active from the start never registered its find field, so the shortcut set the text without focusing it. - **A board beside a schematic answers the net-lines button.** While a PDF 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. - **Connection lines follow the mode button at once, and no longer vanish on 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. - **Connection lines follow the colour scheme.** Light draws the lit net in 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. - **A colour scheme no longer leaks into a board's own settings.** Applying a 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. - **On Light and Atari the selected part is no longer the darkest thing on the 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. - **PDF colours:** *Auto* contrast now lands pale lines exactly at 4.5:1 (it was about 4.35:1). The ◐ and filter menus open in the window the PDF panel is in when it has been popped out. - Deleting a custom scheme counts as a scheme switch, so the dim and the copper colours follow. ### Docs - CLAUDE.md, README and the colour-schemes review corrected against the code (rotation unlock 24°, the Rendering tab, 15 formats, dev port 8082, the licence lines, and what shipped differently from the scheme spec). ## v0.50.1 — 2026-10-11 ### Changed - **Selected pins are clearly marked on Light and Atari.** The coloured glow 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. ## v0.50.0 — 2026-10-11 ### Added - **Three new colour schemes for the board, and Landrex Classic redrawn.** Each of the four owns every colour on the board — pins, part bodies, outlines, the selection, the dim and the copper: - **Dark** — near-monochrome: parts are grey outlines, every pin one quiet grey, ground a dark teal, and colour only for what you select (yellow net, red part). - **Landrex Classic** (not new) is redrawn the way Landrex Test Link drew a board: black, an olive board edge, lavender part outlines, every pin the same blue. Its old black-and-white look is gone. - **Light** — white sheet, dark ink, part tints from the same few hues as the pins. - **Atari (light)** — the 2600's silver label and black ink, every colour taken from the five rainbow-stripe colours. **BoardRipper Default is unchanged** and stays the default. - **The underside has its own pin colour** in the new schemes, so red means power and nothing else. - On the new schemes, part names and pin numbers stay readable over dark pads (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. - **PDF colours** (issue #41): the ◐ button now opens a menu, like the wand 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. ### Changed - **Switching the colour scheme turns the dim off**, on open boards and for new ones. On the new schemes the dim appears only with a selection and fades toward the background instead of black. - On the tinted schemes a large part's fill fades with its size, so a connector covering a third of the board is a tint, not a block. - Drafting Paper, Daylight and Blueprint are replaced by **Light**; an install that had one of them selected opens in Light. A custom scheme is kept as it is. ## v0.49.0 — 2026-10-10 ### Added - **PDFs are sharp at any zoom, on every quality preset.** While you pan or 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×. - **Opening a PDF shows what is happening.** The tab opens at once and says 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 %". - **PDF quality on the start page**, beside Board quality, with the same four names: Quality, Balanced, Fast, Fastest. - **A Board quality preset** — Quality, Balanced, Fast, Fastest — on the start page. Fast and Fastest trade resolution, frame rate and trace corners for speed on tablets. - **Settings ▸ Rendering**, a tab of its own for board and PDF quality, the board text renderer and diagnostics. It was System ▸ Performance & Debug. Smooth wheel zoom and two-finger rotation moved to Input ▸ Navigation. - **Reset all local settings** in Settings ▸ System ▸ Troubleshooting, for a stored setting that left the board blank. Recent files, favourites, history, the session and PDF bookmarks are kept. ### Changed - **PDF zoom is much faster.** On a large schematic at tablet speed, an 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. - **Dense boards draw faster.** Traces and pads are now skipped when they are off screen, and empty part containers cost nothing. - **The app no longer re-renders everything on each board change** — a tap, a selection or a rotation was redrawing every panel. - Two-finger rotation now unlocks at 24°. ### Fixed - **A PDF zoomed past 500 % in Chrome turned black.** The Quality preset drew 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. - **Connection lines stay visible while you pinch** instead of vanishing until the fingers stop. - **CPUs, sockets and connectors** are no longer drawn as mechanical parts with no pin numbers or net names. - **No freeze when PDF text indexing finishes**, and a repeat open shows its first page about 300 ms sooner. - The NR2201ZE family's `indiafix` watermark is filtered by default. ## v0.48.0 — 2026-10-05 ### Added - **Board notes.** A new tab with a book icon in the board sidebar shows 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. - **XZZ test points.** Test points stored outside any part are now drawn, 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. - **Library search.** While you search the Folders view, a list of the 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. ### Changed - **The file pane under the Library** is tidier: open and download are 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. - **Space to flip:** holding it past half a second (was 0.35 s) peeks. ### Fixed - **XZZ diode readings:** BGA balls get their readings (iPhone 14 `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. - **XZZ Chinese text:** part values, names and labels written in GB2312 read correctly instead of being dropped; `Ω` values show. - **"Translate page" crashed BoardRipper** — a panel when picking a pin, the whole window when switching board tabs. Fixed throughout, with a recovery page for any crash outside the panels. - **Rotating a board froze it for a second or two** on dense boards: a 90° turn rebuilt the whole scene. It now only moves the labels that depend on the turn. - **A quick Space tap peeked back** the first time after a board opened, because the flip's own work made the press look long. - **A partial answer from the server** could blank the window (home page statistics) or fail the library load (donor list). ## v0.47.0 — 2026-10-04 ### Added - **Slide mode.** A new ribbon button marks up the board view and saves it 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. - **Minimap.** Zoomed in past twice the fit view, a pan of more than an 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. - **PDF filters in one menu.** The wand on the PDF toolbar opens a menu 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. - **Touch: a right click on glass.** Hold a finger on a part and tap with a second one to open its menu. Any touch outside closes it. ### Changed - **PDFs render faster where it hurt most.** Some PDFs end every page with 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. ### Fixed - **No more blurry blink after a zoom.** When a zoom settled where sharp 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. - **Scrolling over a board next to a PDF works again.** With the PDF the active panel, a wheel over the board took the zoom and drew nothing. A regression from July. - **An old tab after a server update** said nothing and failed to open boards; it now says the server was updated and offers a reload. - **Small net names on the iPad** stopped showing when zooming in; they are back. - **Strip small watermark images** applied to the first pages only after a zoom change; a failed strip left a permanent "reload needed"; a term typed in the filter menu and clicked away from was lost. ## v0.46.0 — 2026-09-29 ### Added - **Back and Forward, like Preview.** ⌘[ goes back to where you were and ⌘] 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. - **A History tab** in the sidebar lists every visit, newest first, grouped 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. - **Viewpoints (off by default).** Switch the Viewpoints filter on and the 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. - **Search history.** The PDF find field and the board search field show your recent searches when you click into them; typing narrows the list, ↑↓ and Enter pick one. ### Fixed - **A single search hit is found again.** With only one hit, pressing Enter or ⌘F after panning away did not bring it back, and a drag that was still gliding pulled every jump off its target. ## v0.45.0 — 2026-09-28 ### Added - **Two-finger rotation.** On a touch screen, turning two fingers turns the 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. ## v0.44.0 — 2026-09-28 ### Added - **Shift+Scroll is the browser's horizontal scroll again (#40).** Every 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. - **Butterfly works on layered boards.** KiCad, Allegro, Altium and EAGLE 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. - **A board loading in the background says so.** Opening a second board 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. - **Edge smoothing has a switch.** Settings ▸ Performance & Debug ▸ Edge smoothing (MSAA), on by default. Off saves GPU work on a machine that cannot afford it, at the price of jagged thin lines. - **A legacy web build for older iPads.** iPadOS 15.4–16.3 gets its own 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. ### Fixed - **Allegro v15: real pin numbers, no invented parts.** A third 15.5.x 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. - **PDF fonts, CJK maps and image decoders are actually shipped.** The 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. - **Tapping a part right after flicking the board selects it.** With inertia 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. - **The touch context menu opens on the iPad.** Press and hold a part. iOS 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. - **Lines on a tablet are smooth again.** The touch performance profile 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. - **The Silkscreen toggle works while Butterfly is on.** Butterfly moves the 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. - **The "gpu idle" readout is gone from the debug HUD.** The value it read is cleared every frame, so it never showed anything. ## v0.43.4 — 2026-09-23 ### Fixed - **Looking up a part or net from a PDF no longer freezes the board for 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. - The hidden-side ghosts of a lit net (the cyan pulsing outlines) are drawn once and pulsed, instead of being rebuilt on every frame. ## v0.43.3 — 2026-09-23 ### Fixed - **The hosted web app now notices new versions on a tablet.** It only checked 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. ## v0.43.2 — 2026-09-23 ### Fixed - **One finger pans the board again after a pinch.** On the iPad, after a 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. ## v0.43.1 — 2026-09-22 ### Fixed - **An unreachable server no longer reads as an empty library.** With the 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. - **"Retrying automatically" is now true.** The unreachable notice used to 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. - A file list that stopped before it finished, with the server otherwise fine, now says so and offers a Reload instead of either a spinner or a false "empty". ### Maintainers - The rules behind this — waiting is not a fact, a promised action needs code 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. - Mockup for the next search work (a typed search and a board lookup side by side in the PDF search bar): `docs/specs/2026-09-22-pdf-search-slots-mockup.html`. ## v0.43.0 — 2026-09-22 ### Updates on Docker Compose installs - **The in-app update no longer rolls back on a Compose install.** Only some 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. - **A failed update leaves evidence.** The helper writes its console to `update-orchestrator.log` in the data directory, and a rollback keeps the failed container as `-failed` instead of deleting it, so its log can be read afterwards. - **One manual pull for affected installs.** An install that was rolling 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. ### Touch screens Every browser on iPadOS is WebKit, Chrome included, so these apply to all of them. - **A pinch is a pinch.** Zooming with two fingers used to select the part 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. - **Both fingers stay on what they touched.** Park one finger and spread 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. - **A tap is a tap.** A finger wanders a few pixels on a tap that feels 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. - **Scrolling a PDF with a finger turns pages.** A drag past the bottom of 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. - **Every touch on a PDF lands.** A drag or pinch that began over the 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. - **Tapping a word in a PDF works on touch.** It was excluded outright. - **A third finger does not freeze the PDF.** Resting a finger on the glass while pinching, as one does when holding a tablet, matched neither the pan nor the pinch path. - **No hover while gesturing.** Hover hit-tests ran on both fingers of a pinch and halved the frame rate on a large board. A single resting finger still reads a pin's net and diode value. - **Tablet performance mode.** On a touch device the board runs at 60 fps, 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. - **Ctrl+wheel on a PDF zooms in steps.** One notch of a mouse wheel took 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. ### Web and offline builds: a folder is the library - **Point the lite build at a folder.** The lite web build and the 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. - **The library survives a reload.** In Chrome and Edge the folder itself is 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. - **Choosing a folder that yields nothing says why.** The browser refuses 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. - **Boards are labelled by their content.** In the folder index every `.brd` file was labelled BDV, because three formats claim the extension. Ambiguous extensions are settled by reading the file header. ### Maintainers - `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 iPad descriptor. 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 helper against a real Docker daemon in three topologies: Compose network, host network, and a new image that never serves. ## v0.42.0 — 2026-09-18 ### Worklist - **The worklist line is a search.** The wrapping row of worklist buttons is 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. - **Naming, clearing and deleting no longer use browser pop-ups.** A new 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. - **Readings appear where there are readings.** A net used to carry three 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. - **The panel follows the theme.** It was the last one painting itself from colours that were not the app's, so it stayed dark while everything else changed. - **The Worklists catalog opens worklists.** In Tools it was a printed list 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. - The row itself is unchanged: the mark and its colours, the water and surge flags, the reference, the note, remove — all of it, in the same order. ### One slider, with or without a switch - **Show-and-size settings are one row.** Pin numbers, net names, diode 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. - **Interactive mode shows its way out.** While it is on, a pill at the top 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. - **The top bar is one row.** The sidebar button is a full-height, darker 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. ### Selected part's pin labels no longer overlap - **Only the pin you point at is big.** Selecting a part used to enlarge every 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. - **Pin and net names on ICs appear later.** The "Pin Labels" floor (Settings ▸ 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 numbers stay inside their pin for longer.** A number sits inside the 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. - **Pin labels fit the pitch.** On a multi-pin part every pin number and net 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 fade in.** Instead of popping in at full strength on one zoom step, 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. ### Fixed - The Tools tab tests reached the sidebar the way the old text strip worked, so they had been failing since the icon rail replaced it. ### Maintainers - The frontend image stage builds on the host's architecture instead of under 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. ## v0.41.0 — 2026-09-14 ### A fresh install that shows something - **The Library fills while the scan runs.** Rows were in the database seconds 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). - **First visit asks to set up the library.** A new install never indexed 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 server answers at once.** Compiling the PDF text engine used to hold the port closed for ten seconds on every start. ### Linking boards to PDFs - **Four rules, in order, editable.** Same name; the board number in the PDF 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. - **Seconds, not hours.** The old matcher compared every board with every PDF, 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. - **The bind picker no longer freezes.** Choosing a PDF for a board rendered 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. ### OpenBoardData - **Download all boards** in Settings ▸ Library — about a hundred boards, a couple of minutes — so readings are already there when a board is opened. - **Boards the Library knows are matched by their resolved board number**, not only by a number in the filename. ### Comparing a component across two boards - **Highlight on board**, off by default: outlines the compared part and marks 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. - **The hover tooltip says what a marked pin differs to** — the other board's name and its net for that pin, coloured the same way as the mark. - **State markers are not identity.** A leading NC / RSVD / TPT / TP / DNU / NU 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. - **Two names sharing a rail prefix read as the same rail** (PP3V8_AON_VDDMAIN 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. - Tooltip text that only restated the colour is gone. ### Library - **Dropping a file onto the library never copies a duplicate.** A byte- 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. ### Docs - README: the build-from-source and dev commands now work as written (go.mod 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. ## v0.40.0 — 2026-09-12 ### Comparing a component across two boards - **Tools ▸ Part comparison** puts one component's pinout next to another, pin 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. - **Right-click a component on the board** for the short way in. "Compare pins" 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. - **A different net name is not always a difference.** Two dumps of one board 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. - **Names that nearly match are shown as a partial match**, with the text the 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. - **Diode readings sit side by side** on boards that carry them, with a mark on 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. - **Pins are matched up before they are compared.** Most boardview formats 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. - **It says when it is not sure.** Most packages look the same rotated or 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. - **With two boards open the pickers fill themselves in**, since there is only one comparison you can mean. The components stay blank — which chip to look at is the actual question. - "Only differences" hides the rows that agree, and Copy puts the visible rows on the clipboard. Widen the sidebar and the two sides get their own columns. ### Top bar - **The sidebar button lines up with the icon column below it.** It sat a few 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. - **The two-window button says 2-Window.** It was an icon whose meaning you had to hover to learn. - **When an update is waiting, the version badge alternates** between the new 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. ### Sidebar - **Auto-hide has a switch you can see.** It sits at the foot of the icon 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. - **The two switches at the foot of the column now say what they are set to**, 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. ### Fixed - **Shift+drag no longer adds a component to the worklist.** Shift+drag zooms 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. ### Maintainers - Shift+drag: committing a drag-to-zoom calls `setPointerCapture` on the container, 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`. - The comparison itself is a pure module, `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. - Tests locate the tool by `data-testid="part-compare"` and its rows by `compare-row` + `data-status`. - Design and decisions: `docs/specs/2026-09-11-part-pin-comparison-design.md`. ## v0.39.2 — 2026-09-10 ### Allegro boards - **The layer fix from 0.39.1 now actually reaches your boards.** BoardRipper 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. - **Copper fills are drawn.** The filled copper areas — ground and power planes — were never read, so Allegro boards showed bare traces over an empty board. Turn them on with "Copper fills" in the Layers panel. - **Components that the file leaves unplaced are now found.** Some Allegro 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. ## v0.39.1 — 2026-09-09 ### Allegro boards - **Some Allegro 18 boards would not open.** They stopped with an "Unknown 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. - **Top and bottom copper are now on separate layers.** On boards that number 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. ## v0.39.0 — 2026-09-08 ### Toolbar and board bar - **The top bar is about the app; the board's bar is about the board.** Side (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. - If you had arranged the board bar yourself, the new controls are placed in front of your arrangement, not inside it. Hide any of them with a right-click on the bar. - **The board bar can float and stand upright.** Right-click it: Position 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. - The board panel's ☰ button now looks like the rest of the board's controls. - **In the vertical bar, Find part and Find net fold into buttons**, a 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. - **The suggestion list stays inside the window.** It shrinks to the room under the field, and opens upward when there is more room above. ### XZZ boards - **Older Apple boards now fold.** In XZZ files from 2008–2015 MacBooks and 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. - **Which way a board folds** is read from the direction each chip's pins run 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. - A file that really is one board with both sides drawn over each other is recognised and left alone. - The board panel offers the folding choice only when there is something to fold. ### Fixed - **The HDR selection outline works in the desktop app and the browser 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 plus any settings change no longer kills the renderer.** With 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. ### Sidebar - **Auto-hide no longer covers the board tabs.** The floating panel starts 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. ### Board panel - **The board's control bar folds away.** The small handle at its right end, 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. - **Hover a tab to see its name.** Library, Settings and board panel tabs show the tab's name under the icon when you hover, the same way the rail does. - **The board bar is editable where it is.** Right-click it to show or hide 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. - **Board panel tabs match the rest.** Info, View or Layers, Search, 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. - **The panel transparency slider is gone.** It appeared next to the ☰ when you opened the panel and was not useful. The ☰ now only opens and closes the panel. ## v0.38.0 — 2026-09-07 ### Sidebar - **An icon rail replaces the sidebar's tab strip.** A narrow column of 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. - **The toolbar's sidebar button cycles** through the open panel, icons 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. - **What you left is what you get back.** A hidden panel keeps its scroll position, expanded folders, search text and the tool you were in. Whether the sidebar was open, and on which tab, now survives a reload. - **Auto-hide.** Right-click the rail, or Settings ▸ Theme ▸ Sidebar 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. - **The status bar can be hidden** with the small toggle at the foot of the rail, in the status bar's own row. - The old text tabs are still there: Settings ▸ Theme ▸ Sidebar navigation ▸ Text tabs. Side, captions and auto-hide are set there as well, for touch screens that have no right-click. ### Library and Settings tabs - **One tab design in both panels.** Icons stay where they are; the open tab's name sits under its icon. Before, Settings put the name next to the icon and pushed the other tabs along. ### Maintainers - Tests locate sidebar and panel tabs by `data-sidebar-tab`, `data-library-tab` and `data-settings-tab`, never by visible text. - Design and decisions: `docs/specs/2026-09-02-activity-rail-design.md`, plan `docs/plans/2026-09-07-activity-rail.md`. ## v0.37.4 — 2026-09-07 ### Selection - **The selected part's outline follows the zoom.** Zoomed out, the white 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. - Only the selected part behaves this way. Hovered parts, parts on the highlighted net, search hits and the ghost outlines keep the fixed padding and width from Settings. - On boards with real pad outlines (XZZ iPhone files, TVW, Allegro) the selection outline hugs the pads. The gap that stayed even at 0 padding is gone. - The HDR outline matches the SDR one in width. ### Diode readings - **Readings are coloured by value.** Near zero is red, 0.2–0.4 V yellow, above that green, and OL blue. The colours are set in Settings ▸ Diode readings. - In "diode only" mode the reading sits on the pin, on top of everything, and is never dimmed. Pin numbers, net names and single-pin designators stay hidden. ### XZZ board packs - **Focusing a part on a packed file no longer lands on empty space.** Search 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. ### Maintainers - The desktop build step runs on Node 22; the packager hangs under Node 26. ## v0.37.3 — 2026-09-05 ### XZZ board packs - **Multi-board XZZ files open properly.** XZZ draws every board twice, top 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). - **The file opens in its own layout**, as XZZ's viewer shows it, with both sides visible. The folded view is one click away in the board panel, and that is where "Swap sides" acts. - **Which half is the top** is read from the copper layers when the file has 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. - Swapping sides rebuilds the scene, and selecting a board fits the whole board. ### Library - **iPhone and iPad boards are recognised by name.** These files are named by 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. ### Selection - **The selection box matches the part outline.** On boards with real pad 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. ### Start page - The HDR selection outline card sits under Interface scale and matches it. 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. - Getting started rewritten and shortened. The format list was out of date, and wrapped list items and bold code rendered broken. ### Landing page - The two editions (Docker / Desktop and Web / HTML) are listed above the menu, with the latest version line. ## v0.37.2 — 2026-09-04 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. ### Browser version, on a tablet - **The file picker offered PDFs only on iPad.** Safari maps each `accept` extension 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. - **Long-press opens the context menu.** The quick actions behind right-click did not exist on touch. Hold half a second on a part or pin. - **Larger touch targets.** The toolbar's buttons rendered 22 px tall — the 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. - **Reload brings the schematic back.** A reopened session restored the board 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. - The wheel-calibration wizard no longer appears on touch devices, and "2 window mode" is hidden where a pop-out window cannot exist (touch, and the offline single file). ### Browser version, first run - A front door: **Open a board or PDF** and **Try a sample board** at the top 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. - **Installable, with an icon.** "Add to Home Screen" on iOS gets a proper 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. - **Updates are announced.** A new version used to be applied silently on the 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. ### Smaller and faster - The main bundle is 21% smaller: the PDF watermark-cleaning library and the glyph-overlay font parser load on first use instead of on every visit. - The PDF worker shipped twice, and the copy actually used was unminified. Now once, minified: 2.2 MB → 1.2 MB, and the offline precache 7.0 → 5.2 MB. - The offline single file parses boards in a worker instead of on the main thread, so a large Allegro or Altium board no longer freezes the tab. - The hosted build's Content-Security-Policy no longer allows `unsafe-eval`. ### Diode-only mode - Single-pin designators (testpoints and pads, drawn directly on the pin) are now hidden along with pin numbers and net names. Designators on part bodies stay. ## v0.37.1 — 2026-09-04 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. ### Formats - **XZZ `.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 `==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`. ### Board view - **Net names can be turned off.** They join pin numbers as their own toggle, 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. - **Diode-only mode.** The diode button on the board overlay now has a third 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. ## v0.37.0 — 2026-09-03 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. ### Formats - **KiCad `.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. - **EAGLE `.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`. - **Board outlines with cutouts render correctly.** An outline is usually 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. ### Updates - **Update checks fall back to GitHub when ripperdoc.de is unreachable.** The 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. ## v0.36.1 — 2026-09-01 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. ### Formats - **XZZ and GenCAD: arcs sweeping past 180° drew as their complement.** An arc 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. ## v0.36.0 — 2026-08-31 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. ### Signing - **Builds can trust more than one signing key.** `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. - **Key rotation now rides the normal update path.** Sign a release with the 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. - **An unverifiable update is now visible instead of silent.** Previously a 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. ### Notes for existing installs 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 . 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. ## v0.35.3 — 2026-08-14 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. ### Formats - **`.fz` boardviews written with old-Mac line endings open.** Some exports end 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` - **A split `.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 `.bdv` bundle 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` ### Rendering - **Highlighting a net no longer draws a wedge across the board.** On boards 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` ## v0.35.1 — 2026-08-07 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. ### Formats - **XZZ board outlines are whole again.** On a lot of boards the outline came 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. ## v0.35.0 — 2026-08-06 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. ### Updates - **What's new survives the update that delivered it.** The release notes used 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` ### Interactive Mode - **A dragged slider no longer hides what it changed.** Interactive Mode 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` ### Formats - **Boardviews that arrive as loose `.asc` files now open as one board.** Some vendor 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 **[@luiztessadri](https://github.com/luiztessadri)** in [#26](https://github.com/AlexeyInwerp/BoardRipper/issues/26). - **ASRock `.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 **[@luiztessadri](https://github.com/luiztessadri)** in [#25](https://github.com/AlexeyInwerp/BoardRipper/issues/25). - **XZZ boards from MSI now show component values.** MSI `.pcb` files write the 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](https://github.com/sjohnson1021))** in [#27](https://github.com/AlexeyInwerp/BoardRipper/issues/27). `721a34b9` - **XZZ through-holes look like through-holes.** A field the parser had 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` - **Oblong pads stop collapsing into fat dots.** The guard that decides 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` - **A capsule pad now looks like one everywhere.** With the pad overlay off, 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` - Found, evidenced across a Switch / PS5 / MSI corpus, and partly prototyped by **Sean Johnson ([@sjohnson1021](https://github.com/sjohnson1021))** in [#32](https://github.com/AlexeyInwerp/BoardRipper/issues/32) — the kind of report that arrives with its own hex dumps and worked examples. ## v0.34.0 — 2026-08-05 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. ### HDR selection outline (experimental, opt-in) - **The selected outline, lit.** Not a glow blob behind the part — the same 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` - **Turn it on where you will see it.** A card on the start page with an 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` - **It asks your eyes, not your monitor.** `(dynamic-range: high)` is a boolean — 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` - **Undock to an SDR monitor and it tears itself down** on the next frame. Rotate, mirror and butterfly all work for free — the layer projects through the same matrix as the board labels. `e774ca67` - **Brightness is a baked ladder, not an opacity.** Measured on real hardware: 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` ### Rendering - **A testpoint's refdes no longer prints on top of its net name.** On every 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` ### MCP - **Flipping the auth switch no longer logs every agent out.** Moving Settings ▸ 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` ### Elsewhere - **An (i) button next to the update badge** says what BoardRipper is, who 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` ## v0.33.0 — 2026-07-29 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. ### The net fans out from the pin you clicked - **Select a pin, get its net.** Under that pin's row, the Info tab lists every 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` - **Everything is one click from everything else.** The chevron opens a 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` - **The previewed component announces itself.** It blinks solid red for about 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` - **Clicking a different net in the pin list switches to it.** Previously that only lit the net on the board while its components stayed out of view. `6a942658` - **It is coloured by your net-line colour.** The selected pin row, the rails, 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` - **Ground is left out.** "Every component touching GND" is most of the board 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` - Pin tables are tighter throughout, and the net's name, pin count and component count now live on the pin row itself rather than a bar of their own. `6a942658` `bb31d55c` ### Fixes - **A much faster cold library load, and a far smaller database.** The files 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` - **The resize popup no longer vanishes when you grab a slider.** With two or 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` - **GenCAD pins are sized from the file instead of a constant.** Every `.cad` pin 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` - **The net tree's connector line no longer breaks.** Its segments butted 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` ## v0.32.0 — 2026-07-27 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. ### Altium Designer support (new, experimental) - **`.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. - **Marked experimental on purpose.** Pad decoding is incomplete: pads whose 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. - Copper pours reuse the existing copper-plane display, so the "Surfaces" toggle in the sidebar controls them like any other format's. ### Tools tab (new) - **Resistor colour-band decoder** — 4/5/6-band, with tolerance and tempco. - **SMD resistor code decoder** — 3- and 4-digit, R-notation, and EIA-96, including the letter-position rule that tells EIA-96 from R-notation apart. - **Capacitor unit converter** — pF / nF / µF in both directions. - **Worklists catalog** — every worklist stored on this device in one list, instead of only the one attached to the open board. ### Library and PDF index - **The Library no longer freezes on large collections.** It used to wait for 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 file list downloads roughly 10× smaller.** It is now gzip-compressed on 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` - **PDF indexing survives restarts** — the background indexer resumes where it stopped, can be restarted from Settings, and picks itself back up automatically after a container update. `d5acd945` ### Fixes - **The PDF watermark filter and the renderer memory fix now actually ship.** 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. - **Panning works again right after a zoom** — a zoom animation could swallow the pan that followed it. - **Net highlight lines redraw on every kind of flip** — they now follow the pads when you flip top/bottom, mirror, or change the flip axis, not only on rotation as before. `efb5bb03` - **The Nets/Parts filter releases the keyboard after you pick a match** — 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` - The net-line pulse no longer rebuilds its geometry every frame. ### Under the hood - Continuous integration is green again. It had been failing on lint for weeks, 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. ## v0.31.44 — 2026-07-24 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. ### Interactive Mode (new) - **Click a board element to resize it** — a toggle at the top of Settings ▸ 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` - **Handles grouped by what you clicked** — a pin offers pin size, pin-number 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` - **Sizes that previously wouldn't budge** — pin, pin-number, net-label, 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` ### Rendering - **Fixed: the renderer could stop on any settings change** — with spotlight (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` - **Fixed: garbage lines across big nets** — net-line, punch-through and glow 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` - **Component names step aside as you zoom in** — a large BGA designator fades to a ghost as it grows, so the net names underneath stay readable instead of being blanketed by it. `1ab634f0` `8babb0cc` - **Fixed: nets on dense BGAs wouldn't select** — a click landed on the net label and selected the whole component, which highlights no net; clicks now resolve to the specific pin. `e1805871` - **Safari/Polaris net-line artefacts** and **hover lag on Windows Firefox**. `6a1777cf` `d7b0ed8e` ### Offline & lite build - **Offline copy** — a single-file `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` ### Library, PDF & Settings - **Donor boards moved under the PDF tab** as a sub-view, mirroring the Folders source menu. `263686e1` - **Fixed: repeated index clicks stacked prompts** — starting a PDF index now 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` - **Settings tabs are icons** that expand to labels when there's room and collapse when there isn't; the search bar moved below them. `077b6827` `6c74fe14` `686d6439` - **Fixed: PDF linked auto-switch** now prefers the active bound board tab. `6e8c8f56` ### MCP - **Fixed: stale 0/0 board descriptor** — the descriptor is re-pushed once parsed data lands. `2b3987b0` ## v0.31.43 — 2026-07-20 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. ### MCP server - **Mixed authentication** — OAuth mode is now a strict superset of token 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` - **Clear errors instead of "oauth 404"** — invalid-token responses now 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` ## v0.31.42 — 2026-07-20 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. ### MCP server - **Per-browser agent tokens** — Settings ▸ Integrations now offers two 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` - **One-time token reset** — on the first start after this update the 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` - **board_sessions** now reports each page's `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` - **Fix:** a background window's periodic bridge reconnect no longer steals the "most recently focused" default target from the window the user is actually working in. `84b91b63` ## v0.31.41 — 2026-07-20 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. ### Board view - **Text fast mode is enabled once for existing installations** (it became 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. - **Search + auto-dim in fast mode** now spotlights the matching parts' labels, matching the classic renderer. - **Fix:** flipping/rotating the board could briefly cull labels against stale transforms. ### Parsers - **XZZ oblong pads** render as capsules (shape 0x01 with width ≠ height), with a plausibility guard against implausibly huge pins (PL5TU1B). `7a474e6a` *(shipped in this release but missing from its original notes; added retroactively.)* ### Performance - **Theme and settings changes no longer rebuild every open tab at once** — inactive tabs rebuild once, when you switch back to them. - **Selection/worklist highlight outlines** skip their redraw on pan frames entirely (previously re-stroked every frame while the viewport moved). - **Net-line pulse** animates a pre-baked layer's opacity instead of rebuilding all line geometry 60× per second. - **Net connection chains** recompute only when the selection actually changes, not on every hover while dimmed. - **Memory:** fixed a leak that kept abandoned board derivations alive after fold/filter toggles; deep-paused background tabs now release the fast-mode text layer's memory too. ## v0.31.40 — 2026-07-19 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`). ### Board view - **Text fast mode** (new default): board text draws on a lightweight 2D 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". - **Smooth wheel zoom** (default on): wheel and keyboard zoom glide toward the cursor at the same speed as before. Disable in Settings ▸ Performance & Debug. Trackpad pinch, Ctrl-zoom and drag-zoom unchanged. - **Selected part labels** now follow zoom level-of-detail; a new "Selected Part Labels" slider (Settings ▸ Zoom Level of Detail) controls their minimum on-screen size. - **Fix:** labels could disappear after changing visual settings while zoomed in — long-standing culling bug, affects both text modes. - **FPS cap is off by default** (the 60 fps cap remains in Settings). ### Performance - **Smoother hovering** on dense boards: pointer handling coalesced per frame, tooltip measurement cached, trace lookup on a spatial grid. ## v0.31.39 — 2026-07-18 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. ### Parsers - **NUL-prefixed GenCAD files open again.** Honhan/GOCCANH "TO CAD" converter 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`) - **XZZ zero diode readings are drawn.** `=0=` records in XZZ diode maps were classified 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`) ### Board view - **Pin numbers toggle in the View tab.** On diode-value maps the pin-number 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. ## v0.31.37 — 2026-07-14 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. ### MCP server - **Open library files into the live view.** New `open_file` tool loads a board (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`) - **Work across several open boards.** New `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`) - **Read the selected component.** New `selected_part` returns the pins and nets of whatever the user has clicked, so the assistant can pick the conversation up from the board. (`439f921e`) - **Search finds a board by any of its metadata.** `file_list` now matches the 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`) - **Don't download boardviews as blobs.** The download tool now tells the 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`) - **Fixed `board_sessions`.** It failed output-schema validation and returned an error instead of the list of open boards; now returns cleanly. (`1d41e169`) ### Library - **Search matches the folder path too.** The Library search box now finds a 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`) ## v0.31.36 — 2026-07-13 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. ### Memory - **Closing a board tab releases its memory.** Two independent leaks meant 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`) - **Live memory readout in the status bar.** Shows precise JS memory (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`) ### Performance - **Board files parse in a background worker.** First open of a large board (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`) - **First paint no longer waits for the board cache.** The parsed-board IndexedDB write (hundreds of ms on big boards) now happens in the background, and a failed write no longer fails the whole load. (`2f1a80a8`) - **Faster parse internals, identical output.** XZZ outline clustering 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`) ### Fixes - **XZZ: 2-pin capacitor/coil pads render full-size again.** Placeholder pad geometry from the parser shrank them. (`7b14c221`) ## v0.31.35 — 2026-07-11 A selection and small-UI fix pass. ### Fixes - **A nearby test point no longer steals a click meant for the component.** From 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) - **Component names no longer render doubled when the board is rotated or 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`) - **Worklist notes wrap in the board hover tooltip.** A long note used to stretch into one very wide line with its own line breaks collapsed; it now wraps and keeps its newlines. (`d9127ee2`) - **The MCP connection Copy button works over plain HTTP.** On a LAN / NAS address (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`) ## v0.31.34 — 2026-07-09 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. ### MCP integration - **The assistant is primed as a repair technician and teacher.** Every MCP 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`) - **Three step-by-step workflows you can invoke.** `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`) - **A built-in repair knowledge base.** The assistant can pull concise method 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`) ## v0.31.33 — 2026-07-09 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. ### MCP integration - **The assistant can see the board and schematic now.** `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`) - **Text access to the open PDF, plus scoped library search.** `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`) - **Pull a PDF down to read it natively.** `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`) - **One-call orientation.** `board_overview` reports the open board, its part/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`) - **Unlabeled nets are flagged low-trust.** Every net the assistant reads is tagged `named` or `synthetic`, so it won't read a function into an auto-generated name like `N$123`. (`cba1b66`) - **Drive-UI actions report their effect.** Highlighting a net, selecting a 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`) ### Fixes - **Large images and downloads no longer drop the bridge.** The board↔assistant 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`) ## v0.31.32 — 2026-07-08 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. ### Performance - **Hidden board tabs release their GPU memory.** Each open board used to keep a 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`) - **PDFs no longer accumulate memory as you scroll.** Long schematics kept every 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`) - **The backend returns freed memory to the OS.** The Go server now derives a 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`) - **Smaller idle footprint elsewhere.** The board renderer frees its overlay 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`) ## v0.31.31 — 2026-07-08 A broad correctness-and-hardening pass from a full multi-directional audit, plus a Library panel cleanup. ### Fixes - **Removed / renamed library files get pruned again.** A deletion-path bug meant 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`) - **PDF text search no longer returns stale hits.** Re-indexing a changed PDF now 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`) - **A failed or rolled-back update can be retried.** An update that rolled back (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`) - **Tighter file serving.** The legacy file endpoints now only serve or delete actual board and PDF files, closing an over-broad path that could reach other files in the data directory. (`1227506`) - **GenCAD boards in non-mil units render correctly.** Files authored in inches or millimetres are scaled properly instead of collapsing to an invisible speck. (`409f570`) - **Better part orientation, honest part types.** Rotated parts from more formats get a correctly-oriented outline box, and formats that can't distinguish SMD from through-hole now report "unknown" rather than guessing "SMD". (`409f570`) - **More robust AI-copilot worklist entries.** Net or part names with different 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`) - **PDF viewer memory and speed.** Fixed a decoded-page memory leak on panel resize and made in-document search noticeably faster on large schematics. (`7efd5b3`) - **Library trees fill in during the first load.** On a fresh load or rescan the Board# and Model views no longer stay empty while files stream in. (`90ac02a`) - **Cloud-storage placeholders fail fast.** Opening a not-yet-materialised cloud file surfaces the "materialize on host" message immediately instead of retrying for three minutes. (`90ac02a`) - **Keyboard and theming polish.** Board rotate/mirror/pan shortcuts no longer fire while a list panel is focused, and bright accent colours stay legible as text on light themes. (`8357207`) ### Library panel - **Reorganised for less clutter.** The Folder tab now sits next to Board #; the 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`) - **DB / Live folder source is a quick dropdown.** Clicking the Folder tab drops a small Database / Live menu that auto-dismisses, instead of taking a permanent row for one control. (`8248438`) ### Under the hood - The MCP live-board bridge is now authenticated and gated, and its OAuth surface is bounded and hidden when the integration is off. Renderer pan/zoom hot paths shed redundant per-frame work. (`1227506`, `c5d4d3a`) ## v0.31.30 — 2026-07-06 Select components hidden under others, and get a clearer read on what's selected versus what's merely on the same net. ### Features - **Select stacked / overlapping components.** When parts overlap (alternates on 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`) ### Fixes - **Clearer selected-component highlight.** The clicked part now draws a bold 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`) - **Worklist mark highlights repaint on toggle.** Turning the worklist Highlight on/off now updates the on-board outlines immediately instead of waiting for the next pan or zoom. (`aeeaced`) - **Worklist scroll stays put.** A long worklist no longer jumps back to the top when you click the board or switch the sidebar tab away and back. (#22, `6961bb8`) - **ASUS X540 BDV boards align to their pins.** Boards whose file supplies no 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`) ## v0.31.29 — 2026-07-01 The session-restore boot prompt now lets you pick which boards and PDFs to reopen instead of restoring all-or-nothing. ### Features - **Session-restore file picker.** The "Reopen your last session?" prompt now 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`) ## v0.31.28 — 2026-06-30 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. ### Features - **Board↔PDF links persist.** Linking a board and a PDF from the tab `∞` now writes 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`) - **Worklist records all three measurements per net.** A net can hold a voltage, 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`) - **Worklist info in the board hover tooltip.** Hovering a part or net that's on the active worklist shows its mark + note inline; net readings render with the diode glyph. (`e459913`, `69bee13`) ### Fixes - **#21 — self-update no longer drops Docker networks.** The in-app update only 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`) - **#20 — PDF pan/zoom is kept when switching PDF tabs.** Switching away and back no longer snaps the view to the top-left. (`8b8dc67`) - **Session restore reopens PDFs.** A restored session reopened boards but loaded PDFs invisibly (no panel); they now reopen, and the "reopened N" toast counts what actually opened. (`f2cf83c`) - **Link indicator + tab polish.** The `∞` / `○○` 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`) - **Clearer menu wording.** Right-click "Donor PDFs" → **"Search in PDF files"**, and the PDF `∞` "auto-open boardview" toggle is renamed **"auto switch boardview"**. (`f2cf83c`, `bc2855a`) ## v0.31.27 — 2026-06-25 Reload no longer loses your work, and the update dropdown now shows what's new before you update. ### Features - **Session restore — reopen your boards + PDFs after a reload or update.** When 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`) - **"What's new" in the update dropdown.** The update badge now shows the new 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`) ## v0.31.26 — 2026-06-25 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. ### Features - **Donor boards.** A new Library tab gathers every PDF marked as a donor, each 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`) - **Marking a donor guarantees it gets indexed.** Adding a PDF to the donor pool 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`) - **The donor pool survives "Reset Database".** A path-keyed JSON snapshot is 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`) - **Per-net inline measurements on worklist rows.** Each net row now carries a 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`) - **Relay-only AI section.** The AI section in the worklist panel is now a relay 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`) - **Source-agnostic `get_measurements`.** The MCP `get_measurements` tool returns all 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`) - **Single off-by-default Highlight toggle.** One **Highlight** button replaces the 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`) - **Hydrate-time migration of legacy measurements.** Opening a worklist saved before Phase 1 migrates any flat-array AI measurement entries onto the corresponding net rows automatically, so existing worklists carry forward without data loss. (`7f2e5cc`) ### Fixes - **Toolbar tooltips no longer spill off-screen.** The top-bar tooltips were 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`) - **Board info sidebar is drag-resizable.** A left-edge handle resizes the overlay 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`) - **MCP bridge stops when MCP is disabled.** Turning MCP off (or removing it by any 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`) ## v0.31.25 — 2026-06-24 Smarter schematic lookup, and two fixes that make ASUS laptop `.cad` boardviews open correctly. ### Features - **Context-scored schematic lookup.** Double-clicking a component or net to find it in a linked schematic PDF now picks the *best* occurrence instead of the first. A scorer ranks every match by page context (the net/pin labels around it), then designator font size, then on-page proximity — so a large BGA lands on its **schematic symbol sheet** rather than a shared pin/connector index table, even though its reference designator sits far from the radiating net labels. Purely-numeric pin tokens (the sequential 10, 11, 12… a BVR exposes) are dropped from matching since they pack tables and carry no signal; ball-style alphanumeric names are kept. Verified across the 8 largest BGAs on an Apple M1 820-02020. (`9b6996b`, `2bc2cb4`, `84a1548`) ### Fixes - **ASUS `.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`) - **ASUS `.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`) ## v0.31.24 — 2026-06-23 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. ### Features - **Worklist AI-mode feedback loop (agent ↔ worklist ↔ user).** The agent writes worklist entries, marks, and list notes, *requests specific measurements*, and posts relay messages; the user answers (or skips) each measurement inline and types back through a prompt box the agent reads. New live MCP tools — `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`) ## v0.31.23 — 2026-06-19 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. ### Features - **MCP server — analyse and drive a board from an AI agent.** A standards-compliant Model Context Protocol server (Streamable HTTP at `/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 over MCP, including descriptions.** `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`) - **Repair-helper skill.** A bundled Claude Code skill (`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`) - **One-click connect + OAuth onboarding.** Connect with a bearer token or zero-token **OAuth 2.1** (approve in the browser, nothing to copy). The Integrations tab has per-client connect cards (Claude Code, Claude Desktop, Cursor), a live status line with connected-page and tool-call counts, and a **Test connection** button. (`e9e8556`, `ed9ff71`, `5de9a60`) ## v0.31.22 — 2026-06-18 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. ### Features - **Light themes.** Three new themes — Drafting Paper, Daylight and Blueprint (light) — join the dark set. Body text now auto-flips to a dark graphite pair on light backgrounds, the secondary surface/border tiers shade the right direction (lighten on dark, darken on light), and a semantic token layer (scrims, tooltips, hover/active surfaces, highlight washes, the neon binding chips) tracks the theme instead of baking in dark literals. A WCAG visibility test guards every theme against unreadable / white-on-white text. (`5273252`) - **Custom theme editor.** A single editable theme: tune the interface, the board canvas, the pin-group colours, the net-label styling and the component fills, all in Settings ▸ Theme. Editing any colour while a built-in theme is active forks it into your custom slot (one confirm) and switches to it — built-in themes are never mutated. (`0f80877`, `d8878f1`) - **Pin colours by net class.** Pin fill colours are now grouped — Power / Ground / Datalines / Logical / Misc — each an editable list of keyword→colour rules (comma-separated, with `*` 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`) - **Editable net-label background + shadow toggle.** The net-name label background box (previously a hardcoded black at 60%) is now an editable colour + opacity, sitting next to the net-label font colour in the Theme tab. A new "Pin Label Shadow" toggle (Board ▸ Labels) controls the drop shadow on pin numbers and net names — net names were previously always shadowed with no way to turn it off. (`c1be911`, `3c89432`) ### Changes - **Landrex is a true black-and-white theme.** It now behaves like any other theme (selectable, editable, forks-to-custom) but renders fully monochrome: greyscale pins/parts, no net/component colour, white labels on a black board. Readable — no more white-on-white — and one pin-group edit away from colour if you want it. (`9e3cea6`, `6c22b0b`) ### Fixes - **Board labels re-tint live on a theme switch.** Switching to a theme that changed no render setting (the light themes) updated the canvas background but left the already-drawn pin/net/part labels their old colour, because the scene rebuild was gated on render-settings the light themes don't touch. (`c269ffd`) - **Dockview tab strip stayed dark on light themes.** Dockview silently adds its built-in "abyss" theme class, whose hardcoded dark variables out-specificity ours and froze the tab header dark. (`c35f666`) - **Theme overrides now apply on board load.** A board opened under a saved override-theme (Landrex, or a custom theme carrying pin colours) rendered with un-merged settings until the next theme switch — `setActiveBoard` only recomputed for per-board overrides, not theme ones. (`6c22b0b`) ### Performance - **Colour editing no longer blocks the UI.** Each colour-picker change forced a synchronous full scene rebuild on every drag-frame; rebuilds now coalesce ~140 ms after the last change and run off the input handler, so the picker stays responsive on dense boards. (`6883f17`) ## v0.31.21 — 2026-06-16 Two features: PDF orientation controls and a diode-value channel for XZZ boards. ### Features - **PDF rotation, mirror & page modes.** PDF documents gain per-document 90° rotation (Q/E, or ⌘←/→), a horizontal mirror (⌘↑), and a single-page / continuous page-mode toggle (default continuous; rotation forces single but restores your page mode when un-rotated). Rotation flows through every viewport so fit-width, the click/highlight transform, and the tile grid all adapt; mirror is a wrapper flip that carries the page, highlight, and overlays together. The orientation controls sit just right of the page-switching group, and the board↔PDF link (∞) control now lives in the PDF tab. (`fe6d9f4`, `2de7955`, `8bbed00`) - **Diode-value channel for XZZ boards.** XZZ "Middle layer diode value" `.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`) ## v0.31.20 — 2026-06-13 Point fix for the board-side link menu shipped in v0.31.19. ### Fixes - **Board-tab ∞ link menu opened off-screen.** The dropdown is mounted inside the dockview tab header — a clipped / transformed container — so the in-flow absolute menu was clipped and `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 `` 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`) ## v0.31.19 — 2026-06-13 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. ### Features - **Drop-to-incoming with auto-organisation.** Files dragged onto the app are now routed into `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`) - **PDF indexer re-queues on modify + manual Force re-index.** The scanner now marks a PDF pending again when its bytes change, and Settings ▸ Library exposes verbose index status plus a Force re-index control. (`d7e014d`) - **Board↔PDF linking from both sides.** The ∞ control is now a full link menu on the board tab *and* the PDF toolbar (it was PDF-only); the 5-second auto-close that kept dismissing the menu mid-decision is gone, the unlinked state reads "Link board…", and a "Boardview" section header was added. (`325afda`) - **PDF paging that keeps your place.** The page-number box commits on Enter/blur instead of navigating per keystroke (typing "250" no longer visits 2 then 25); PageUp/PageDown page even while the search field is focused; new Home/End jump to first/last page; a zero-result Ctrl-F shows `0/0` with a one-click handoff to the library-wide PDF search. (`0495267`, `42a34f9`) - **'?' shortcut overlay.** Press `?` 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`) - **Worklist + hidden-parts affordances.** Shift-click add/remove now toasts both directions and only force-opens the sidebar on first use; hiding a part shows an Undo toast and a "Hidden parts (N) / Restore all" row in the Layers tab so a hidden part is recoverable. (`0495267`, `db72f69`) - **First-contact polish.** The Library boots to the Board# view when history is empty, empty states point at the right action, dropped files that match no parser now say so, and the welcome modal's "Skip for now" no longer means "never again". (`567366a`) - **Unrecognized files keep their filesystem layout** under a labelled divider in the Board# tree instead of collapsing into one bucket. (`1f66453`, `7a5268f`, `3e57b38`) ### Changes - **Settings restructured for coherence.** Input/System fields commit immediately (matching the home-screen Quick settings); the Preview/Apply/Cancel footer and Reset render only on the Board tab they govern; the Navigation section now leads the Input tab; the Library tab splits into "Scanning & Indexing" and "Database info"; software-update moved to its own System-tab section with a Check-now button and the drop-to-update recovery note; Library Sync collapses with an on/off summary; settings search shows a "no matches on this tab — N on X" hint. (`98de9d5`, `bb4d2e1`) - **Single-layer boards open on Info.** The in-tab sidebar now defaults to component detail (Info) on boards with no layer stack, and Layers on multi-layer boards; Info is first in the strip. (`30c1d9a`) - **SearchTab handles dense boards.** The part/net lists are memoized and capped at 400 rendered rows with an overflow hint, so the search tab no longer janks every keystroke on 5–8 k-part boards. (`f0bf6d7`) - **Library housekeeping** — stats/progress moved below the search row, scan/index actions moved to Settings, collapse-all relocated to the statsbar, the failed-index modal gained filename + path, brand grouping is case-insensitive with a canonical display label, and the dead "Save as BVR3" toolbar button was removed. (`8e64479`, `48c672f`, `bc49a0b`, `f0bf6d7`) ### Fixes - **Update badge showed `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`) - **Interface-scale slider jitter.** The conditional "Reset" button appeared/disappeared as the value crossed 100% and shifted the track width mid-drag. Removed it; reset is now double-click on the slider (which previously did nothing here). (`bb4d2e1`) - **Library stats line jumped between tabs.** It sat below the filter input, which only renders on non-PDF tabs, so the line shifted up a row on the PDF tab. Pinned directly under the tab row. (`bb4d2e1`) - **Adversarial-review regressions** from the UX work — board-tab link indicator vanishing when the bound PDF wasn't open, a Settings re-sync subscriber clobbering global slider baselines, a collapsed-only summary chip, split-view scoping of "Restore all", and a stale fixed-position dropdown — all fixed before ship. (`db72f69`) - **Sorted-section gating** in the Board# tree now requires both brand and model (and `resolution_status === 'resolved'`), and the re-resolve gate triggers on any metadata-field change. (`70433d7`, `0fa860d`, `7005f75`, `73aec8c`) ## v0.31.18 — 2026-06-11 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. ### Features - **Streaming library load with visible progress.** Opening the library on a NAS install with ~80 k files used to block the main thread for several seconds — `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`) - **Hold Space to peek at the other board side.** Tap Space still flips top↔bottom permanently (existing behaviour). Hold Space — past a 350 ms threshold — flips while held and returns to the original side on release. Casual taps never trigger peek; the gesture is autorepeat-gated so holding doesn't re-flip every tick, and a window blur mid-hold drops the press state so an alt-tab doesn't strand the next tap. A small floating hint chip ("Peeking other side — release Space to revert") appears bottom-center once the threshold trips, auto-vanishes 3 s in, and is suppressed entirely for taps under the threshold. PDF-panel context (Space → fit-to-width) unchanged. (`ff36fbe`, `47d239f`, `e1f3549`, `5ad47e7`, `da114ee`) - **Landrex theme — selection halo overlay suppressed.** The yellow halo overlay that lit every pin on the selected net was overpowering against Landrex's lower-contrast pastel palette and made the actual board geometry hard to read. Suppressed in that theme; pin recolouring still runs so the selected net is distinguishable by pin colour alone. (`b291ad5`, `f8ea9b4`) ### Fixes - **streamChunks auto-commit bug truncated warm load at chunk size.** The IDB cache walk's cursor handler was `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` 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`) - **Rescan no longer leaves the library showing stale data without progress.** Two interacting bugs: `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`) - **PDF click highlight no longer bleeds into the adjacent pin label.** `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`) - **PDF click highlight no longer triggers a parent re-render / focus loss.** Moved the highlight rendering to an imperative path so a single click doesn't propagate through React's reconciliation and blur the focused element. (`112e773`) - **ODM fallback in the Board# metadata tree.** Files whose `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`) ### Performance - **Folder tree cached in IDB so warm loads are instant.** On a warm reload the file list came from the chunk cache in ~100 ms, but `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`) - **Streaming-load diff cleanup.** Post-audit pass: drop the dead getRaw/put cache methods, single-tx cursor walks where they're safe to use, parallel stats+meta fetch (saves one RTT on warm TTFB), in-place `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`) ## v0.31.17 — 2026-06-08 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. ### Fixes - **Legacy Mac build now renders PDF text again.** v0.31.16 polyfilled `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`) ## v0.31.16 — 2026-06-08 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. ### Fixes - **`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`) ## v0.31.15 — 2026-06-07 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. ### Features - **Global interface scaling factor (50–150%).** A new slider, exposed in two places: the **Theme** tab of Settings (after the chrome / accent / background pickers) and a dedicated centred row directly under the welcome banner on the start page. Mechanism: `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`) ## v0.31.14 — 2026-06-07 Follow-up to v0.31.13. The load-overlay UI lockout had a second source on the linked-PDF auto-load path. ### Fixes - **Linked-PDF auto-load no longer hijacks the board load-progress overlay.** `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`) ## v0.31.13 — 2026-06-07 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. ### Fixes - **Load-progress overlay no longer blocks the UI.** Three compounding problems: the overlay was reusing the self-update modal's CSS (position:fixed inset:0 backdrop-blur z-index 99999), the X / Dismiss button only rendered when the load was `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`) ## v0.31.12 — 2026-06-07 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. ### Features - **Copper fills overlay (TVW Surface blocks).** New "Show copper fills" toggle in the layers / view sidebar renders ground planes and power pours from each copper layer of a TVW file. Tints with the matching layer colour at 0.25 alpha so the trace network still reads on top of the dim fill. Multi-layer wiring (per-layer container under `surfacesLayer`) follows the same emphasis machinery as traces. (`e96077a`, `2119ccf`, `b012c24`) - **Per-file rotation + mirror overrides, persisted.** New IndexedDB store (`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`) - **Per-file load-progress overlay.** Cold opens (29 MB TVW, encrypted XZZ, etc.) now show phase-level timing: Downloading → Cache lookup → Reading file bytes → Parsing → Post-process → Writing cache → Building scene. Each phase carries elapsed-ms and free-form log entries; the overlay stays open through `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`) - **Re-open-same-file is instant + safe.** Clicking an already-loaded file in the library doesn't re-download or re-parse it — focus jumps to the existing tab and the load overlay dismisses cleanly instead of hanging at "Downloading…" forever. (`a94f55e`) ### TVW + Allegro + XZZ pad geometry - **TVW pad/pin doubling fixed; chamfered pads render as their real outline.** The earlier doubling artefact — circle pin showing through a copper rectangle on every chip — went away by routing the pin layer through the parser-supplied `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`) - **XZZ pin emission now carries real pad geometry.** XZZ `.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`) - **2-pin parts (caps / inductors / resistors) honour the real pad rectangle.** Synthesised FlexBV-style rect was wider than the real SMD pad on every TVW/Allegro/XZZ file, so the rect poked out as a halo around the pad overlay. Pin loop now uses `pin.padBounds` for 2-pin geometry when supplied, falls back to FlexBV synth for BVR/BDV/CAD/Mentor. (`e41a379`, `9cd6638`) - **2-pin pad overrides (per-type `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`) - **Pads-off restores the classic FlexBV circular pin look.** Reversing the earlier "pin always draws pad shape" decision: pin sprite is a classic circle inscribed in `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`) - **Pad overlay z-order + colouring overhaul.** Pads draw BELOW the pin layer on multi-layer boards (so net-coloured pins win) and ABOVE on single-layer XZZ; in both cases pads are now coloured per-net (via `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`) - **Pad toggle is instant.** Earlier intermediate state baked the pin sprite shape into the pin Graphics at build time, so toggling pads triggered a full scene rebuild. The current implementation pre-builds inscribed-circle pins once and just flips `padsTop.visible` / `padsBottom.visible`. (`1720872`) - **Part outline + selection rectangle include real pad extents.** `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`) ### XZZ orientation - **Every X-fold XZZ board has been rendering horizontally mirrored.** Root cause: the original X-fold patch (`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`) - **X-fold boards land 180° off after the mirror removal; fixed by branching auto-rotation.** Removing `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`) - **Axis-aligned chips with diagonally-oriented pads no longer get diagonal outlines.** XZZ pad-angle-majority detection (commit `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`) - **Perimeter guard tightened to actually catch BGAs.** The first cut required ≥40% of pins on the AABB perimeter — fine for small chips, fails for big BGAs where most pins sit inside the grid (UN000 has 25%, UF500 27%). The `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`) ### Other fixes - **Selection halo for pads-off matches the pin sprite.** Selection redraw path was always using `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`) - **pdfindex per-file failures log path + concrete cause.** Earlier `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`) ### Internal - **PARSER_VERSION 67 → 72** across the release: 67→68 (TVW poly polygons), 68→69 (cache surfaces), 69→70 (XZZ debug probe), 70→71 (axis-aligned chip guard v1), 71→72 (guard v2 dropping onAny). Each bump invalidates entries that baked stale derived state into IndexedDB; first open after upgrading re-parses, subsequent opens hit cache normally. ## v0.31.11 — 2026-06-06 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. ### Features - **Real pad geometry from XZZ pin sub-blocks.** Previously framed as "no pad polygon data" — wrong. The pad rectangle (width, height, shape byte, rotation) is in the variable-length region of the 0x09 pin sub-block past the pin name; the old parser read four fields and skipped 32 bytes of "unknown" data which turned out to be three repeated copies of the same (u32 w, u32 h, u8 shape) chunk, with rotation 8 bytes earlier. A2442 mainboard yields 20,046 pads (8,620 round BGA balls + 11,426 rect SMD) feeding the existing `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`) - **Per-part silkscreen outlines.** Each XZZ part block carries four 0x05 sub-blocks on layer 17 — the four edges of the silkscreen rectangle drawn around the component. The parser emits them as four 2-point `SilkscreenPath`s 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-wide silkscreen routed to the overlay.** Top-level layer-17 segments (board logo art, polarity dots, the legacy print on every board) now flow into `board.silkscreen` instead of being discarded. Chained via `chainByComponent` so the sparse art renders with few GPU draw calls. (`33c8184`) - **Via blocks (drill + annular ring + net).** 0x02 sub-blocks parsed: 17,273 vias on A2442 with diameter and net. The renderer's via overlay matches connected layers to nearby trace endpoints regardless of the layer-pair field (which is flag-coded `1, 5` on every surveyed Apple file with no observed variance — blind/buried stack-ups unrecoverable until a counter-example shows up). (`e365add`) ### Fixes - **Rotated XZZ chip outlines follow the chip, not the AABB.** A 45°-rotated multi-pin chip (e.g. N3842 on A2442 — a 19-pad diamond) was drawn with an axis-aligned selection box because `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`) - **XZZ file-wide X-mirror after butterfly fold.** A whole class of Apple files came out X-flipped on the screen after the butterfly unfold. The pin-direction detector now runs after the fold and corrects, so the file-wide orientation matches the physical board. (`56cdd26`) - **XZZ mirror axis after renderer auto-rotation.** Vertical-oriented XZZ files that the renderer auto-rotates 270° were mirroring on the wrong axis (X-mirror on screen instead of Y-mirror, or vice versa) after the rotation swapped scene and screen axes. The mirror now picks the correct axis post-rotation. (`c8fdb42`) - **PDF-driven selection respects `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`) ## v0.31.10 — 2026-06-05 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. ### Fixes - **2-Window Mode: focus the popout when adding a PDF to an existing group.** When mode is ON and a popout already exists, `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`) ## v0.31.9 — 2026-06-05 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. ### Features - **2-Window Mode toggle.** New toolbar button (next to the sidebar/library toggle, labelled "2 window mode") detaches the PDF Dockview group into a separate OS window. Library + BoardView stay in the main window. Toggling off re-docks the PDF into the main grid; closing the popout window via the OS close button does the same and flips the mode off. Persisted to localStorage so the preference survives reloads — the first PDF you open in the new session reopens in a popout automatically. Icon flips between `IconLayoutBoardSplit` (single-window, internal splits) and `IconBoxMultiple` (multiple stacked windows) to communicate state. (`6a8f7d2`, `8e3963e`, `6fa6069`, `f69e011`, `cccd299`, `193cb52`, `f726735`, `9bae652`) - **Link maintained across windows.** Because Dockview popouts share the parent's JS context, the existing board↔PDF click-to-jump, BindLink dropdown, PDF↔PDF cross-link, and auto-switch-linked-panel behaviour keep working with no IPC. Clicking a designator on the board (main window) still jumps the PDF in the popout. Mode is symmetric across the browser build and the Electron desktop app — same primitive on both. (`6fa6069`, `cccd299`) - **Cross-window plumbing.** Theme classes on `` 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`) - **Electron child-window chrome.** `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`) ### Fixes - **Redock must not destroy the pdf.js document.** `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`) ### Cleanup - **Worklist button removed from the toolbar.** It's still reachable through the sidebar tabs and the keyboard shortcut; freeing the toolbar slot lets the new 2-Window toggle sit next to the sidebar/library toggle without crowding. (`193cb52`) ### Tests - **17 new tests in `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`) ## v0.31.8 — 2026-06-03 PDF open path broken on older browsers — single-edit hotfix to the vendored pdf.js worker. ### Fixes - **PDF `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. ## v0.31.7 — 2026-06-03 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. ### Features - **Settings search.** Top-of-panel filter box (or press `/` 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`) - **Net-line colour chain.** Net Lines section exposes the primary and chain-adjacent colours as a two-swatch row `[primary] → [adjacent]`. Both fields were already read by the renderer but had no UI. (`27dae0e`) - **Min Label Size — single slider.** The four-control "Active Size S/M/L + three tier sliders" apparatus collapses to one "Min Label Size" floor. Migration picks whichever tier was active and assigns its value to the new field so previously-tuned settings carry across the upgrade. (`2db9073`) - **Mechanical parts: auto-skip body fill.** EMI shields, heatsink frames, and oversized through-hole connector shadows are now auto-detected (footprint contains ≥5 other component origins, description mentions SHIELD/HEATSINK/FRAME, or trailing-dot duplicate set) and rendered without a body fill so the smaller components beneath stay visible. Border outline and pins still draw normally. Right-click any part for a per-component override. `PARSER_VERSION` bumped so stale IndexedDB caches don't hide the change. (`ca318e9`, `a5eebe8`, `415b4de`) ### Cleanup - **Settings tooltips and section labels.** Label Atlas Resolution default tooltip fixed (8, not 12). PDF Pan Boundaries default tooltip fixed (true, not OFF) — and the toggle moves out of [Debug] into PDF Viewer ▸ Navigation. "Pin Colors by Net" section renamed to **Pin Color Rules** so the name no longer collides with the new net-line colour chain. Dead `selectionHalo` field dropped. (`d0b9ebe`) - **Board fill controls united.** "Use board metadata color" toggle moves from Theme tab into Board ▸ Board Outline, sitting next to the Board Fill alpha slider it was already paired with semantically. (`d0b9ebe`) - **Library tab visual unification.** Library Folder & Database, Library Sync, and OpenBoardData now all use the same collapsible-chevron treatment instead of three different visual shapes. (`e53fd6c`) - **HomeBackdrop cache controls removed.** Settings is the single source for cache wipes; the Quick settings card no longer carries a parallel set of buttons. (`d0b9ebe`) ### Infra - **Desktop zip mirror on ripperdoc.de + landing-page rework.** Desktop artifacts now publish to the public mirror alongside the signed update pipeline; landing page restructured for the new format. (`47154eb`) ## v0.31.6 — 2026-05-31 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. ### Features - **Pin button on every search result row.** Each component AND net row in the sidebar Search tab now has a trailing `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`) - **Nets are first-class worklist entries.** The `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`) - **Net-specific mark vocabulary: short / solved / absent.** Separate `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`) - **Right-click pin a net** — the net chip in the board context-menu header now carries the same `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`) ### Internal - New store API: `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`) - New `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`) - New `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`) ### Known limitations - **Canvas highlight for worklist net entries is not yet wired**. The current `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. ## v0.31.5 — 2026-05-30 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. ### Features - **Sidebar Search tab is now usable as a navigation panel.** Sections (Components / Nets) and expanded spoilers stay sticky at the top of the scroll container so you can collapse them from anywhere in the list. Component rows expand to show every net the part touches, sorted by the first pin that hits it (mirrors the existing net→components sublist). Both sublists now show the connecting pin id as a compact monospace chip. Net rows in the Nets section now navigate (zoom/pan) on click; previously they only highlighted. Row spacing tightened (28→22 px). Chevrons upgraded to Tabler icons at 18 px so the expand affordance is actually visible. Expansion is decoupled from `boardStore.selection` so clicking a net inside an expanded component (or vice-versa) no longer collapses the spoiler. (`2893d19`, `9e9145c`) - **New Settings ▸ Navigation ▸ Navigate-to-component controls.** Two new knobs that affect every navigation entry point (sidebar Search, standalone Search panel, Worklist, NetList, NetsDropdown/PartsDropdown, PDF cross-target click): (`845b0a0`, `e483f6b`, `44b3a0b`) - **Component Size** (0.05–0.90, default 0.25) — fraction of the smaller viewport dimension the component should fill after navigating. - **Zoom Mode**: *Auto* (default) keeps the current zoom when the part lands in the 1.5%–70% comfortable band and snaps only when extreme; *Keep* never changes zoom, just pans; *Always* snaps to Component Size on every click. - **Disco mode peak alpha is now 1.0** — at the top of each heartbeat the part is solid red, then fades back out over the silent 70% of the cycle. Reads as an actual blink instead of a faint tint. (`e134f4b`) ### Fixes - **Removed the 3× fit-to-board relative cap inside `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`) - **Ghost button icon set is now consistent across all three states.** Previously the *off* state used `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`) - **autoZoom too-small threshold lowered from 5% to 1.5%** of the smaller viewport dim. At fit-to-board zoom on a typical 100 mm board, even mid-sized ICs land below 5% of screen — so the old threshold caused *Auto* mode to fall through to the *Always*-snap branch for almost every click, defeating the point. New threshold means only sub-pixel passives trigger a zoom-in; mid-board ICs stay at the user's current zoom and just pan. (`44b3a0b`) ### Internal - **CI is green again** for the first time since v0.31.0. Two `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`) - **Lint warning sweep: 103 → 12** via four parallel agent passes covering non-overlapping scopes (test specs / PDF subsystem / sidebar+toolbar / panels+renderer). Highlights: - 4 real React 19 *"ref accessed during render"* bugs fixed in `SettingsMockup` and `SettingsPanel/PartTypeRow` — the ref pattern was rewritten using React's official "previous-render state" pattern (`useState` + render-time comparison + `setState`). - 9 stale-closure dep bugs in `PdfViewerPanel` callbacks (`syncTransform`, `rescaleWrapperChildren` missing from `useCallback`/`useEffect` arrays — real correctness issues, not just lint noise). - 3 new sibling files extracted from oversized components: `Sidebar.utils.ts`, `BoardSidebar.utils.ts`, `panels/board-viewer-bridge.ts`. - All `any` casts at pdf.js boundaries replaced with structural `as unknown as { ... }` casts; pdf.js / type-decl mismatch documented inline. - 12 remaining warnings are all `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`) - **New Playwright test** `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`) ## v0.31.4 — 2026-05-30 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. ### Features - **First-run "Set up by gesture" interactive welcome.** A modal walks you through four bindings (Board Pan/Zoom, PDF Pan/Zoom) by asking you to *demonstrate* the gesture you want in a shared test window — scroll, swipe, pinch, or drag is detected and bound via `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`) - **Disco highlight mode on the ghost button.** The hidden-side ghost toggle is now a three-way cycle: off → ghosts → disco. In disco mode every part on the highlighted net (both sides) heartbeats red — a body-fill flash + outline ring tied to a threshold-clamped sine so ~70% of each second is silent and only the top ~30% blooms. Same-net only; no selected net means no halo. The button hue-rotates while active, and the animation is disabled under `prefers-reduced-motion`. (`76701ce`, `3294553`, `82d3085`, `228a841`, `ee14b99`) ### Fixes - **Start-page supported-formats list now matches the parser registry.** The list previously claimed `.xzz` (wrong — XZZ PCB is `.pcb`) and omitted `.bv`. Updated with dual-purpose notes for `.brd` / `.cad` / `.bdv`. (`5c06d4d`) ### Internal - **Verbose Debug-panel gesture recognizer** kept alongside the welcome modal for research: raw event dump, classification reasons, momentum stats, per-gesture confirm-and-apply. `recommendInertia` direction stays in lock-step with the welcome checkbox semantics. (`5c06d4d`) - **Renderer refactor:** `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`) - **Disco silent-phase short-circuit:** `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`) ## v0.31.3 — 2026-05-29 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. ### Features - **Multilayer trace layers — bump/pin a layer to the top, and reveal it on select or pin.** On butterfly/multi-layer boards you can promote a copper layer above the others; selecting or pinning a layer now reveals it (a bare select reveals transiently, only a pin flips its persistent visibility). (`67dcd9c`, `b2a0e09`, `85b2650`) - **PDF viewer defaults: Pan Boundaries ON and Standard render mode out of the box**, with zoom locked to fit-width while Pan Boundaries is active so the view can't bounce off the page edge. (`1b50386`, `c965a31`) ### Fixes - **Mentor PADS Layout binary `.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`) - **TVW: recover the parts list when the probe/fixture skip overshoots.** On some Landrex/Gigabyte boards a probe body declared a garbage element count, running the parser cursor to EOF and silently dropping every component (board opened with copper but no parts/pins/nets). A parts-section scan now relocates the list — the same recovery the net table already uses. (`315e0ae`) - **Renderer: type-hidden parts are truly hidden, and the MEC shield ref-designator prefix is handled.** (`d0136ec`) - **UI hardening: AZERTY keyboard shortcuts, a panel error boundary, and a unified ComponentInfo** across its two render sites. (`ac94e40`) - **Backend: update-progress no longer 500s** (gzip Flusher/SSE fix), plus exact-byte content dedup and zero-byte-file skip during scans; BOM reason labels de-duplicated. (`deb43ef`, `42c2d22`) ### Internal - **DevOps/CI:** `.dockerignore`, CI Go 1.25, release-signature self-verify, and a native-amd64 boot smoke-test gate before publishing. (`271ea48`, `048f33e`) - **Tests/docs:** XZZ parser spec with sample-fixture skip-guards; documentation corrections (watermark terms, deps, store names, boards.db). (`da41437`, `657a9cf`) ## v0.31.2 — 2026-05-26 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. ### Fixes - **Fix update rollback on installs with a large pre-v0.31 PDF index.** The v0→v1 pdf-index migration drops the legacy in-process `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`) - **Note on v0.31.1:** its `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. ## v0.31.1 — 2026-05-26 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. ### Fixes - **Fix boot failure: `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`) - **Build backend by cross-compiling per `$TARGETARCH` from `$BUILDPLATFORM`** instead of QEMU-emulating the target toolchain during multi-arch buildx — faster and removes an emulation variable from the build. (`c4e2482`) ## v0.31.0 — 2026-05-26 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. ### PDF text search (new backend index) - **Backend pdfium/wazero text index with SQLite FTS5 — replaces the old in-process rsc.io/pdf extractor.** PDF text now lives in a separate `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`) - **Streaming search results with live progress.** `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`) - **Dedicated PDF Search tab** with donor-scoped search, a donor-list management view, folder-scoped indexing ("Index this folder"), and a backend progress UI with ETA/rate and failed-file drill-down. (`f602566`, `a67bf8e`, `1457c11`, `b622427`, `16bd267`, `ca1125d`) - **Watermark terms sync to the backend** and offer a reindex of affected files when changed. Container memory floor raised to 1 GB; Go base bumped to 1.25 for the wazero runtime. (`77c1b96`, `f94134c`, `3168ffa`) - **Search fix:** prefix-match terms so a partial part number matches its variants (`AOZ5332` → `AOZ5332QI`). (`8bc3ebe`) ### Library content deduplication - **Duplicates are detected and marked during the filesystem scan**, so each scan yields a clean, deduped file list and the PDF indexer never re-extracts byte-identical copies. Detection is size-bucket + sampled hash (`sha256(size ‖ full-file ≤192 KiB else head/mid/tail 64 KiB)`) — a unique byte size is never read. (`51311e7`, `34920e1`, `86ea2f1`, `58f0094`, `b92d939`) - **Parallel hashing + same-(name,size) representative.** The dedup phase hashes with a worker pool and reads one representative per same-named/sized cluster instead of every copy — much faster on large libraries, still byte-exact. (`08f2b6f`) - **The PDF indexer enumerates only canonical files** — non-canonical duplicates never reach its work queue. (`815bdeb`, `88c007d`, `3d5df43`) - **Content-oriented views collapse duplicates; the Folder view shows everything.** Board#/Model and search results show one canonical row + an "N copies" badge across the whole view (even across board numbers); the on-demand "Find duplicates" pass + stats live in Settings ▸ Library. (`f152df3`, `2be750e`, `b3e6e4b`, `c2c814d`, `8844394`, `2525c13`) - **Reset PDF Text** wipes the index by recreating `pdfindex.db` (O(1), not a multi-minute row delete) and is no longer gated on a running file scan. (`61f626d`, `6b8c514`, `10a0c3a`, `9055b75`) ### Library views - **Board# view regrouped Brand → Model → Board#**, with same-named files under a board number folded into an expandable spoiler so re-saved copies from different sources don't clog the tree. The Model tab is hidden (its grouping is now redundant). (`85c2e03`, `f485694`) ### Scan - **Auto-binding is off by default** (opt-in via `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`) ## v0.30.13 — 2026-05-25 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. ### PDF↔PDF cross-lookup - **Link two open PDFs and cross-probe designators between them.** When a board is only available as PDFs (e.g. a schematic sheet and a layout sheet), open both and link them 1:1 from the PDF's bind (∞) menu — a new **Cross-link PDF** section after the board bindings. Single-clicking a component designator in either PDF jumps the linked PDF to a matching occurrence and highlights it — reusing the existing search → snap-to-match → highlight path — and re-clicking the same token cycles through multiple matches. Fully bidirectional. Text (vector) PDFs only: matching is on the designator string, so no nets or pins are needed. The link is symmetric and persisted across reloads. (`62297b1`, `ecff231`, `6191dca`, `de9c710`) - **Cross-lookup feedback as a toast.** "No match for X in Y" and "Linked PDF not open" surface as a toast rather than inline toolbar text, which had collided with the search hint and broken the toolbar layout. (`de9c710`) ### Selection - **Cmd/Ctrl+C copies the selected component, pin, or net name** to the clipboard, so a designator can be pasted straight into notes or a search. (`c00e1af`) ### Files - **Drag-dropped boards and PDFs are saved into the library's `incoming/`** instead of living only in the browser session, so a dropped file is kept for later. (`224a00a`) ### FZ parser - **Don't trust `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`) ## v0.30.12 — 2026-05-25 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. ### Worklist - **"Connections" highlight — see which nets a group of parts share.** The worklist's old one-way "Highlight" button is now a **Connections** toggle: turning it on selects every part in the active worklist (cyan) and glows every net shared by two or more of them, so the interconnections inside a repair set stand out at a glance. Toggling off clears both — the one-click un-highlight the panel was missing — and the highlight also clears automatically when its source worklist is switched, wiped, or deleted. No connecting lines are drawn for these shared nets; net-lines stay reserved for an explicitly selected net. (`aad93b1`) - **Selecting a 2-pin component in hierarchical net-line mode now lights both pins.** Picking a 2-pin part by its body — which previously highlighted nothing — seeds the highlight from one pin's net, so the one-hop adjacency carries it through to the other pin's net and both chains draw. (`aad93b1`) ### Net lines (hierarchical mode) - **Per-part-type "bridge" override — carry the hierarchy through >2-pin parts.** The chain-adjacent mode previously hopped only through 2-pin parts. A new **Bridge** checkbox in Settings ▸ Part properties lets a whole part type pass the propagation regardless of pin count, so 4-pin Kelvin/current-sense resistors — and, when enabled, 3-pin transistors — carry the trace. Resistors, **inductors, and diodes** bridge by default; every other type is off and toggleable. (`0bd6f0e`, `0836107`) - **Editable hierarchy depth.** The propagation depth is no longer hard-coded — a **Hierarchy Depth** slider (1–4, default 2) at the top of Part properties controls how many hops the highlight follows down a series chain, and changing it updates a live highlight immediately. (`0836107`) ### CAD (GenCAD) parser - **Place bottom-side parts correctly (`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`) - **Correct top/bottom via pin-majority.** Allegro2CAD `.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 parser - **Clear error for encrypted BRD_V1.0 boardviews.** Opening a `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`) ## v0.30.11 — 2026-05-24 Two parser fixes surfaced by ASUS G513R laptop boards (FZ and GenCAD exports of the same `6050A3348801/03` design), plus a Landrex theme refinement. ### FZ parser - **Decode the GOCCANH "GCVN" variant via a raw-deflate fallback.** ASUS boards re-exported by the GOCCANH Vietnamese tool decrypt to a `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`) ### CAD (GenCAD) parser - **Surface the `$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`) ### Theme - **Landrex is now a board-only high-contrast style.** Switching to Landrex Classic no longer repaints the interface chrome — its `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`) ## v0.30.10 — 2026-05-20 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. ### Allegro parser - **Honour `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`) - **Derive component side from the assembly-graphic subclass.** Some exporters leave the `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`) - **Skip footprint-definition via-in-pad templates.** `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`) - **Skip footprint-definition copper-track templates.** Same root cause for traces: `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`) ### PDF viewer - **Pan boundaries off by default; the panel no longer sticks mid-scroll.** Multiple users reported the PDF panel getting "stuck" on a page mid-document or refusing to scroll in some positions, traced to the pan-clamp logic interacting with the page-flip threshold. `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`) ## v0.30.9 — 2026-05-19 Two self-update papercuts found while shipping v0.30.8. ### Updater - **Default `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) - **Drop-to-update accepts `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`) - **Friendly redirect when the wrong tarball is dropped.** Releases publish `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`) - **Drop overlay copy mentions update bundles.** Previously read "Drop board or PDF files here" — no hint that the drop-to-update path existed at all. Now lists boards, PDFs, and the expected bundle filenames. (`b8bfcab`) ## v0.30.8 — 2026-05-18 Right-click context menu redesign and two parser fixes. ### Context menu - **Density pass.** Font 12px, padding 4px 10px (was 0.9em / 6px 14px); the action strip is gone, replaced by inline chips inside the existing header row. Menu caps at 360px wide; long PDF / board donor names ellipsis-truncate with the full path available on hover (`title` attribute). The whole menu reads ~30% shorter at the same content. (`ac6fcc8`) - **Header chips.** Each value (part / pin / net under the cursor) renders as a chip with a copy-on-click affordance; search-on-web and worklist-pin sit as 22px icon buttons on the chips that own them. No more "pin F11" prefix — the value alone speaks in chip context. The previous three-row Copy / Search / Worklist strip is gone; everything an action could target now lives next to its name. (`ac6fcc8`) - **Pin chip leads + lit state for worklist membership.** The worklist pin sits FIRST inside the part chip, before the value name. When the right-clicked component is already in the active worklist the pin renders filled + accent-coloured (`IconPinFilled` on `.is-lit`), tooltip switches to "click to remove", and the click removes the entry — mirrors shift-click on the canvas. (`ac6fcc8`) - **Donor row: chevron on the left, variant connector hairline.** The variant-expansion glyph moved from a 2em right-side icon (often misread as "submenu →") to a 1.4em left-side chevron in a fixed 18px slot, matching the file-tree disclosure pattern. Expanded variants now render inside a wrapper with a 1px left border that visually ties them to their donor row. (`ac6fcc8`) - **Auto-expand donor groups when total donor rows < 5.** Small lists default to open — no extra clicks needed for the typical "one bound PDF + one sibling board" case. The chevron still toggles, and explicit user collapses persist for the lifetime of that menu open. (`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`) ### Parsers - **BRD parser early-rejects the proprietary `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`) - **FZ alt-end fallback for non-canonical `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`) ## v0.30.7 — 2026-05-18 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. ### PDF - **Wand toggle preserves the list across reloads.** Pre-fix: clicking the wand button OFF wrote `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`) - **Centralised through `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`) - **Migration recovers users currently stuck with empty lists.** Existing localStorage entries with `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`) - **Context menu's "Add to watermark filter" now auto-enables the filter.** Adding a term implies the user wants it filtered now, so the right-click action sets `pdfWatermarkFilterEnabled: true` alongside appending the term. Previously, adding a term while the wand was off silently extended the list with no visible effect. (`e664291`) ## v0.30.6 — 2026-05-18 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). ### FZ - **`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`) - **Typed `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`) - **In-app FZ-key dialog (`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`) - **No new CSS systems.** The dialog reuses the existing `.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.** `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. ## v0.30.5 — 2026-05-17 PDF tile render path — single perf change with measurable settle-time win on fresh-tile renders. ### PDF - **Pipeline tile `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`) ## v0.30.4 — 2026-05-16 PDF viewer feel — two complementary changes aimed at making zoom and multi-page navigation smoother on heavy schematics. ### PDF - **Render-mode switch (Auto / Standard / Always-tile).** New control under Settings ▸ PDF ▸ Render mode. *Auto* (default) keeps the existing behaviour — tile above 1.05× zoom for crisp deep-zoom text, full-page below. *Standard* always renders the full page into one canvas (Firefox-style) — smoother during pinch/zoom and one fewer compositor layer; pixels go soft past the browser's canvas-max dimension (~5–6× on A4). *Always tile* is a debugging escape hatch. The router lives behind a `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`) - **Gesture-suspend during wheel-zoom bursts.** Adaptive-throttle renders inside `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`) - **Min zoom floor dropped 1.0 → 0.5 to unstick multi-page navigation.** v0.27's fit-to-width zoom lock (`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`) ## v0.30.3 — 2026-05-16 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. ### Worklist - **Per-row waterdamage flag.** Each entry has a binary "water damage observed" toggle alongside the existing mark cycle. Dim/transparent droplet icon when off, cyan-blue when on. Independent of the mark state — a part can be both water-damaged AND replaced. Roundtrips through the clipboard via a `[water]` token on the row (`R12[replaced][water] (note)`). (`762cfe5`) - **Per-worklist ticket note.** A `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`) - **Mark-cycle flash chip rendered through a React portal.** Previously the per-row "Replaced / Reworked / Cleaned" popover anchored to its button via `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`) - **Custom soldering-iron icon for the "Reworked" mark.** New `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`) - **"Select" button renamed to "Highlight"** — matches what it does (load worklist parts into the cyan canvas overlay, not a real selection mutation). (`4bdc9ad`) ### PDF watermark filter — complete rewrite The v0.4.2 – v0.30.2 implementation passed a `Set` 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: - Adds a `watermarkFilter: string[]` option to `PDFPageProxy.render(...)`, forwarded through `_pumpOperatorList` → `GetOperatorList` worker message → `Page.getOperatorList` → `PartialEvaluator.getOperatorList`. - In the evaluator's main switch, between `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. - At `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: - **NFKC normalisation** so Latin ligatures like `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. - **Right-click → "Hide as watermark"** context-menu item on PDF text. Adds the clicked text to `pdfWatermarkFilter`; the existing filter-change subscription flushes caches and triggers a re-parse with the new term. - **Default filter expanded** to `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`. ### Hover tooltip - **Value and Package surfaced in the hover tooltip.** On boards whose parsers fill `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`) - **Trace-hover label cleaned up.** Was `Top · pin trace` (awkward). Now `trace · Top` (or just `trace` when no layer name). Net stays on line 1. (`07f018f`) ### Clipboard - **Worklist Copy + context-menu Copy work over LAN / NAS / Tailscale, not just `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 `