Show background jobs in the phone's layout, not a popover #166

Merged
logan merged 3 commits from feat/62-jobs-as-a-notification into main 2026-08-20 22:32:47 +00:00
Collaborator

#73 Phase 3's first item, and the one #57 is blocked on: the job
indicator needed somewhere to live before the top bar can go.

Commits

commit issue what
502b814 feat(jobs): let a panel answer for every kind, at either density #62 job-panel gains kinds="*", density, active-only
23f5a0c feat(shell): show background jobs in the phone's layout, not a popover #62 <job-band>, the shell grid row, the indicator standing down, 5 specs
f76ee96 docs(jobs): the phone's band, and why it is in flow CLAUDE.md + the measurement that decided the shape

What it does

Below 600px the header job-indicator stands down and <job-band>
takes over, as a grid row between the top bar and the main panel.

It is the existing job-panel — so pause, cancel, Details and the log
all come along, and so does applyJobControl, which is what carries
the "you will discard hours of downloading" confirmation for an index
build. A surface drawing its own buttons drops that silently; #27
already named that trap.

The report did not reproduce, and the spec says so

The issue's motivation is that the popover "is obscured by other UI, so
it cannot be read while jobs run". I could not reproduce that in any
tier available to me
, and I looked before designing anything:

viewport jobs panel clipped? elementFromPoint at its centre
424×439 (the device's own) 1 no, 325px of room to spare job-indicator
424×439 4 no, 259px spare job-indicator
390×780 6 no job-indicator
1100×800 1 no job-indicator

So this is not a stacking fix, and jobs-on-a-phone.spec.ts says
so in its header rather than asserting a property that was never
broken here. What justifies the change without that symptom:

  • a popover is a disclosure, and background work is the one thing a
    phone should not make you open something to see;
  • #57 is blocked on this issue and needs the bar emptied, which a
    popover anchored to that bar cannot deliver.

The device is no longer available to check the original symptom against
Chrome 113, and I have not claimed it is fixed.

The design changed because the suite said so

The first version was wrong, and this is the useful part of the PR.
#62 says a job notice is "the Transient/Persistent shape", and the app
has exactly one notification surface, so the panel went into
notification-host's fixed band. It renders correctly, sits on top and
stays inside the viewport — and it is unusable. At 424×439 a compact
panel showing two jobs is ~216px of a 439px screen, drawn over the
content with pointer-events: auto, so it swallows every tap
underneath.

make ui-test (955 tests), tsc, lint and the whole Go suite passed
on that version. What caught it was four e2e specs, none of them
about jobs
:

✘ header-action-overflow › offers every action from the menu on a phone
✘ phone-shell › opens the full-screen now playing, and comes back
✘ phone-shell › opens the full-screen now playing for a track with no art
✘ top-bar-fit  › no control sits outside the bar at 390px, with a job running

Three were click timeouts on the band. So the band is in the layout
now — it pushes rather than covers, which is #24's one sentence ("no
action is ever unreachable at any supported size") deciding a layout
question. A band that hides the app in order to say the app is busy has
traded the popover's fault for a worse one.

Two things fell out of that and are now properties of the code:

  • A finished row in flow is furniture, holding the content down
    after the work is done — hence active-only. Settings keeps finished
    rows, because that is where "did the last scan work" is asked (#27).
  • job-row already had the right density. compact is described
    in its own source as "the popover density", which is exactly what
    this replaces: 216px against 259px for the same two jobs.

Verification

The specs bite

Five new specs in jobs-on-a-phone.spec.ts. Reverting the three source
files:

✘ are shown in the band, without opening anything
✘ leave the top bar, which is what #57 is waiting for
✘ push the content down rather than covering it
✘ are not inside the live region they sit beside
✓ do not leave a second panel behind on a desktop

The fifth cannot fail against the old code — there was never a panel —
and guards the bottom-nav trap for the future. Component tier, same
treatment: answers for every kind when asked with a star and passes its density to the rows both fail without the change.

push the content down rather than covering it is the one that would
have caught my own first attempt: it asserts the main panel's top moves
and clears the band's bottom, and that a hit test at the band's centre
returns the band rather than something of the app's underneath it.

One accessibility point worth flagging

A running job repaints several times a second, and the notification
stack it would have shared is role="status" aria-live="polite" — a
progress bar in a live region is a screen reader reading a number out
over and over. The band is deliberately not one, and there is a spec
asserting it is not nested in one either.

The suite

make e2e 192 passed (187 before, +5), make ui-test 955 (952
before, +3), make lint (3 configs, 0 issues), make test (3 configs),
tsc --noEmit in both packages, make css-check, make bindings-check,
make skill-check.

Formalities: make test, make lint, make bindings-check — this
branch touches no Go and no bindings. make css-check is not a
formality here and earned its place: it caught a backtick inside a
comment in a css literal, which ends the literal.

Scope deliberately left

The Direction's second half — "consider an actual Android system
notification for long work" — is not here. The issue itself says it
needs its own argument, and the foreground service currently exists to
keep audio playing; extending it to scans is a battery and permissions
question, not a UI one. Happy to file it if wanted.

Closes #62

#73 Phase 3's first item, and the one **#57 is blocked on**: the job indicator needed somewhere to live before the top bar can go. ## Commits | commit | issue | what | |---|---|---| | `502b814` feat(jobs): let a panel answer for every kind, at either density | #62 | `job-panel` gains `kinds="*"`, `density`, `active-only` | | `23f5a0c` feat(shell): show background jobs in the phone's layout, not a popover | **#62** | `<job-band>`, the shell grid row, the indicator standing down, 5 specs | | `f76ee96` docs(jobs): the phone's band, and why it is in flow | — | CLAUDE.md + the measurement that decided the shape | ## What it does Below 600px the header `job-indicator` stands down and `<job-band>` takes over, as a **grid row between the top bar and the main panel**. It is the existing `job-panel` — so pause, cancel, Details and the log all come along, and so does `applyJobControl`, which is what carries the "you will discard hours of downloading" confirmation for an index build. A surface drawing its own buttons drops that silently; #27 already named that trap. ## The report did not reproduce, and the spec says so The issue's motivation is that the popover "is obscured by other UI, so it cannot be read while jobs run". **I could not reproduce that in any tier available to me**, and I looked before designing anything: | viewport | jobs | panel clipped? | `elementFromPoint` at its centre | |---|---|---|---| | 424×439 (the device's own) | 1 | no, 325px of room to spare | `job-indicator` | | 424×439 | 4 | no, 259px spare | `job-indicator` | | 390×780 | 6 | no | `job-indicator` | | 1100×800 | 1 | no | `job-indicator` | So this is **not** a stacking fix, and `jobs-on-a-phone.spec.ts` says so in its header rather than asserting a property that was never broken here. What justifies the change without that symptom: - a popover is a **disclosure**, and background work is the one thing a phone should not make you open something to see; - **#57 is blocked on this issue** and needs the bar emptied, which a popover anchored to that bar cannot deliver. The device is no longer available to check the original symptom against Chrome 113, and I have not claimed it is fixed. ## The design changed because the suite said so **The first version was wrong, and this is the useful part of the PR.** `#62` says a job notice is "the Transient/Persistent shape", and the app has exactly one notification surface, so the panel went into `notification-host`'s fixed band. It renders correctly, sits on top and stays inside the viewport — and it is unusable. At 424×439 a *compact* panel showing two jobs is **~216px of a 439px screen**, drawn over the content with `pointer-events: auto`, so it swallows every tap underneath. `make ui-test` (955 tests), `tsc`, `lint` and the whole Go suite passed on that version. What caught it was **four e2e specs, none of them about jobs**: ``` ✘ header-action-overflow › offers every action from the menu on a phone ✘ phone-shell › opens the full-screen now playing, and comes back ✘ phone-shell › opens the full-screen now playing for a track with no art ✘ top-bar-fit › no control sits outside the bar at 390px, with a job running ``` Three were click timeouts on the band. So the band is **in the layout** now — it pushes rather than covers, which is #24's one sentence ("no action is ever unreachable at any supported size") deciding a layout question. A band that hides the app in order to say the app is busy has traded the popover's fault for a worse one. Two things fell out of that and are now properties of the code: - **A finished row in flow is furniture**, holding the content down after the work is done — hence `active-only`. Settings keeps finished rows, because that is where "did the last scan work" is asked (#27). - **`job-row` already had the right density.** `compact` is described in its own source as "the popover density", which is exactly what this replaces: 216px against 259px for the same two jobs. ## Verification ### The specs bite Five new specs in `jobs-on-a-phone.spec.ts`. Reverting the three source files: ``` ✘ are shown in the band, without opening anything ✘ leave the top bar, which is what #57 is waiting for ✘ push the content down rather than covering it ✘ are not inside the live region they sit beside ✓ do not leave a second panel behind on a desktop ``` The fifth cannot fail against the old code — there was never a panel — and guards the `bottom-nav` trap for the future. Component tier, same treatment: `answers for every kind when asked with a star` and `passes its density to the rows` both fail without the change. `push the content down rather than covering it` is the one that would have caught my own first attempt: it asserts the main panel's top moves *and* clears the band's bottom, and that a hit test at the band's centre returns the band rather than something of the app's underneath it. ### One accessibility point worth flagging A running job repaints several times a second, and the notification stack it would have shared is `role="status" aria-live="polite"` — a progress bar in a live region is a screen reader reading a number out over and over. The band is deliberately not one, and there is a spec asserting it is not nested in one either. ### The suite `make e2e` **192 passed** (187 before, +5), `make ui-test` **955** (952 before, +3), `make lint` (3 configs, 0 issues), `make test` (3 configs), `tsc --noEmit` in both packages, `make css-check`, `make bindings-check`, `make skill-check`. **Formalities:** `make test`, `make lint`, `make bindings-check` — this branch touches no Go and no bindings. `make css-check` is *not* a formality here and earned its place: it caught a backtick inside a comment in a `css` literal, which ends the literal. ## Scope deliberately left The Direction's second half — "consider an actual Android system notification for long work" — is not here. The issue itself says it needs its own argument, and the foreground service currently exists to keep audio playing; extending it to scans is a battery and permissions question, not a UI one. Happy to file it if wanted. Closes #62
logan added 3 commits 2026-08-20 22:18:33 +00:00
Three properties the phone's band needs, added here so it is the same
panel rather than a second job UI -- which is what keeps
`applyJobControl` and its "you will discard hours of downloading"
confirmation in the picture.

`kinds="*"` is every kind, which is what the header indicator was for.
Spelled as a star rather than taken as the meaning of an empty
attribute, because empty is what a typo and a dropped binding both
produce and "show everything" is the wrong thing to do by accident;
empty still shows nothing.

`density` is passed to `job-row`, whose `compact` variant its own
source calls "the popover density" -- which is exactly what the band
replaces. `full` stays the default, so the four settings call sites are
untouched.

`active-only` drops terminal rows. The band is in the layout, so a
finished row there holds the content down after the work is done;
Settings keeps them, because that is where "did the last scan work" is
asked and a finished row there dismisses itself.
The header indicator is a disclosure anchored to a bar 3.25em tall on a
screen 439 CSS px tall, and it was reported as unreadable behind other
UI. Background work is the one thing a phone should not make you open
something to see, and #57 deletes the bar it hangs from and is blocked
on it having somewhere else to live. Below 600px the indicator stands
down and <job-band> takes over.

It is the existing job-panel at `kinds="*"`, so pause, cancel, Details
and the log come along, and so does applyJobControl.

**It is in the layout, not over it**, and that was measured rather than
assumed. The first version put the panel in notification-host's fixed
band: it renders correctly, sits on top and stays inside the viewport,
and is unusable -- at 424x439 a compact panel showing two jobs is
~216px of a 439px screen, drawn over the content and swallowing every
tap under it. Four e2e specs caught it, and none of them was about
jobs: two phone-shell journeys and the header's action menu, all
failing on clicks the band was intercepting. As a grid row above the
main panel it pushes instead, which is #24's one sentence deciding a
layout question -- a band that hides the app to say the app is busy has
traded the popover's fault for a worse one.

It renders nothing above 600px, from matchMedia rather than a media
query, because that decides whether the element exists: Settings
already holds four job-panels and a fifth answering for every kind is
bottom-nav's "resolved to 2 elements" trap again. index.css keeps it
display:none off the phone for a second reason -- an in-flow grid child
with no named area is auto-placed into one of the shell's rows, which
is what the skip link is absolutely positioned to avoid.

top-bar-fit's 390px case asserted the indicator was up, so that it
could not pass by measuring the idle case under another name. At phone
width it is now deliberately away, so the assertion takes the other
branch of the same rule -- the indicator is hidden, the band has the
row, and the bar still has nothing hanging out of it -- rather than
the width being quietly dropped from the list.

The report's own symptom is deliberately not asserted anywhere: it did
not reproduce in this tier. Measured at 424x439 the popover was neither
clipped nor covered, so a spec claiming a stacking fix would be
asserting something that was never true here. The spec says so.

Closes #62
docs(jobs): the phone's band, and why it is in flow
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m26s
CI / e2e (pull_request) Successful in 8m4s
f76ee96ac4
CLAUDE.md's jobs section said the header indicator is "the one view of
everything at once, from every page"; that is now true on a desktop
only, and the band is the phone's half.

NOTES.md takes the measurement that decided the shape -- an overlay
band at 424x439 is a lid, not a notification -- and the corollary about
which tier can see it: ui-test, tsc, lint and the Go suite all passed
on the broken version, and what failed was three e2e specs that have
nothing to do with jobs. Run the suite, not the spec you wrote.
logan merged commit 8879192097 into main 2026-08-20 22:32:47 +00:00
Sign in to join this conversation.