# How ipx was built The long-form record, newest first: what landed each day, what was wrong before, and what it cost to find out. [CHANGELOG.md](../CHANGELOG.md) has the short version, by release; this is where the reasoning lives. New write-ups go at the top. See [README.md](../README.md) for what the thing is. ## 2026-09-11 — One meaning per icon, sortable columns, and one player Ray asked for a pass over the whole UI: consistent icons, and buttons placed next to what they act on. The pass started from screenshots of every view and dialog, taken against the browser suite's own daemon and fixtures. Several things did not agree: - Unsubscribe was a minus in the toolbar but an x in the feed header. There it read as "close this page", and x also meant Cancel in every dialog. - Half the dialogs still had word buttons: Log, Add feed, Users, Unsubscribe, OPML, Popular's Subscribe, the Copy buttons and Sign out. - The player bar and the folder arrow were font characters, and so was the row's "playing" marker. Those came out thin and varied from font to font. - The OPML subscription page laid out its header differently from a feed's, and All Subscriptions had no buttons at all. The rule now is one meaning per icon. Minus unsubscribes, x closes or cancels, plus adds, subscribes or imports, and a dialog's confirm button carries the icon of what it does. The words go in the tooltip. The toolbar is grouped by what it acts on: add, unsubscribe and scan act on feeds; play, read and keep act on the selected item. Its read and keep buttons now show that item's state, as the item's own buttons already did. A browser test opens all eight dialogs and fails on any button without an icon and a tooltip. It also checks that the toolbar and the page use the same icon for the same action. The pass turned up three bugs: - **Escape ignored inside a dialog's text box.** The keydown handler returned early for any key typed in a text box, and Escape sat below that check. So Add feed, which opens with the cursor in its URL box, could not be closed with Escape. It showed up because the screenshot run stalled: the dialog stayed open, and every later click landed on its overlay until the run timed out. - **A white password box in the dark theme.** The input rule listed text, search and number fields, but not password ones. - **A stray "• •" under an item with no date.** Each detail carried its own trailing dot, whether or not the next one was there. All Subscriptions gained Mark everything read, as `POST /api/read-all`. It marks the feeds from the person's subscriptions, which is the same set the view's query is scoped by. It asks first, since one click covers every feed. **Sorting** happens on the server. A list arrives fifty at a time, so sorting in the page would only have reordered what was loaded. The column name picks one of a fixed set of SQL expressions (`order_sql`), so nothing the browser sends reaches the query, and an unknown name means newest first. Ties also fall back to newest first, so "Load more" does not skip or repeat items at a page boundary. A database test runs every column both ways. Size came out of the File column into its own, and small files show KB, since whole megabytes made every article image "0 MB". **The double play.** Ray found that pressing play in the Files pane played the file twice at once. The pane drew its own `