Wrap table cells instead of cutting them off, and hand a batch over in one request

The renderer draws every table cell at one line with an ellipsis, so most
of a table was unreadable on a phone -- and an elided cell looks exactly
like a short one, so nothing said anything had been cut. Cells now take as
many lines as they need and align to the top of the row. Width is the other
half: a column narrows to 136dp and no further, and past that the table
scrolls sideways rather than squeezing. 136 is the widest floor that still
fits three columns across a phone, measured rather than picked; four and up
scroll, which is the right answer for genuinely too many columns.

The import screen used to send one request per selected row, so a handover
was only as atomic as the network: some rows started and the rest were
never asked for, and a row nobody asked for looks exactly like a row nobody
picked. `POST /setups/{id}/importable/delete` and `.../import` now take the
whole list, and every id is registered as in flight before the 202 goes
back. Only the registering is atomic -- the work settles per row, since six
deletes that all roll back together is not something a filesystem offers.

The echo driver grows `/table N`, with cells long enough to have been
truncated: a fixture of tidy one-word values renders fine whether or not
the bug is there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-31 21:27:20 -04:00
1 parent 3c159fa1e1
commit 778b2e3b04
7 files changed
+336 -93

No files matched your search

+30 -7
View File
@@ -207,13 +207,22 @@ first if a remote spawn ever mangles an argument.
and says which operation in a word** -- `BusyItem`, used by both the
session list and the import list so the appearance is learned once. The
word rather than a bare spinner because "deleting" and "importing" differ
in kind, and the inertness is the overlay consuming pointer events rather
than each caller remembering to disable its own click handler.
- **Importing and deleting run on the server, not in the request.** `DELETE
/setups/{id}/importable/{session}` and `POST
/setups/{id}/importable/{session}/import` both answer 202 and do the work
in a spawned task, because the phone that asked is free to leave and used
to cancel its own batch by doing so. What replaces the reply is
in kind. It dims and desaturates but does **not** make the row inert: the
caller disables its own click handler while it passes a label. An overlay
consuming pointer events was tried and swallowed the drag along with the
tap, so a list could not be scrolled while anything in it was busy.
- **Importing and deleting run on the server, not in the request, and a
batch is handed over in one call.** `POST
/setups/{id}/importable/delete` and `POST /setups/{id}/importable/import`
each take a list of session ids, answer 202, and do the work in spawned
tasks -- because the phone that asked is free to leave and used to cancel
its own batch by doing so. A list rather than a route per session because
one request per row made a handover only as atomic as the network: some
rows started and the rest were never asked for, and a row nobody asked
for looks exactly like a row nobody picked. Every id is registered as in
flight before the 202 goes back. Only the *registering* is atomic; the
work itself settles per row, since six deletes that all roll back
together is not something a filesystem offers. What replaces the reply is
`session::pending`: every row of the listing carries `pending` and
`error`, and `GET /setups/{id}/importable/events` streams the changes.
**Both, not either.** The stream is a broadcast with no memory, so an
@@ -246,6 +255,20 @@ first if a remote spawn ever mangles an argument.
be there to import again" is exactly the one the switch makes false. The
server deletes the machine's copy *first*, so a machine it cannot reach
leaves the session where it was instead of half-deleted.
- **A markdown table wraps its cells and never cuts one off.** The
renderer's own defaults draw every cell at one line with an ellipsis,
which on a phone loses most of a table -- and an elided cell looks
exactly like a short one, so nothing on screen says anything was cut.
`Markdown.kt` supplies its own header and row blocks with `maxLines =
Int.MAX_VALUE` and `TextOverflow.Clip`, cells aligned to the top of the
row so a two-line cell does not re-centre its neighbours. Width is the
other half: a column narrows to 136dp and no further, and past that the
whole table scrolls sideways rather than squeezing -- 136 because it is
the widest floor that still fits three columns across a phone, which is
the commonest table there is. Exercise it with the echo driver's
`/table N` (default six columns), which writes long cells on purpose:
a fixture of tidy one-word values renders fine whether or not the
truncation is fixed.
- **Android Lint is not optional and is not run by a build.** It found a
crash that had been shipping: `java.time` on a minSdk-24 app with
desugaring off — and later a permission check that silently dropped every