Commit Graph
8 Commits
Author SHA1 Message Date
iris-aiandClaude Opus 5 7bf10061e2 A build waiting to be installed arrives with a sound
Every notification this app posts was on one IMPORTANCE_LOW channel, so
all of them were silent -- including the row for a build that landed
while nobody was looking, which is the one that carries the Install
button and is the only thing here that is news rather than a report of
something somebody just pressed a button for.

A second channel rather than raising that one, because Android lets an
app only ever lower an existing channel's importance: raising "work" in
code would do nothing whatever on a phone that had already run this app,
and the whole fix would be somebody finding it in Android's settings.

The waiting row also has to undo the setOnlyAlertOnce every other row
wants. It replaces the running row it grew out of -- same tag, same id
-- and an update to a notification posted alert-once is silent however
loud its channel is, which is exactly the arrival nobody is watching
for.

Measured on the API 36 emulator: pressing Update on a test project and
going to the home screen posts the row on channel=ready, importance 3,
with mSound resolved and mIsInterruptive=true, and the shade files it
above its own "Silent" divider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 22:32:18 -04:00
iris-aiandClaude Opus 5 adaeb0c0ad A finished build keeps its notification, with an Install button on it
Handing an APK to the system installer means starting an activity, and
Android refuses one from the background: fired anyway it does nothing at
all, so a download that finished while the phone was in a pocket read as
an update that never landed. `updateComponent` now stops at
ReadyToInstall whenever this screen is not in front of anybody -- exactly
as it already did for the second APK of a project-wide update -- and that
state is drawn as a row of its own: no bar, dismissible, out of the
group, and carrying an Install action whose PendingIntent is the install
intent itself. An activity is the one press a notification may make from
any state.

Which is why the drawing moves from the service into WorkNotice: that
row outlives the last running thing, and something still alive has to
take it down. The list screen is that something. The service is now the
keep-alive and the one notification it is held up by, nothing else.

Two things had to be true for the row to come down again, and neither
was. continuePendingInstalls now begins by forgetting a download whose
install has already happened -- the installed copy's lastUpdateTime
against the downloaded file's mtime, rather than listening for an
install, since the same broadcast reports the removal the wrong-key case
is waiting for. Without it, pressing Install on the notification and then
returning to the app ran the whole install a second time; measured, not
feared. And registerPackageChangeReceiver dropped every arrival that was
an update: EXTRA_REPLACING marks the ADDED half of a replace and the
REPLACED that follows it, not only the REMOVED half the guard was written
for, so an in-place install reached nothing here until the screen next
resumed.

Verified on the emulator: Install pressed, app sent to the home screen,
build and download finish in the background, the row turns into
"Downloaded, waiting to install..." with the button, the service stops,
Install from the shade puts up the system installer, and the row goes
away by itself once the install lands -- with the app still in the
background, and with no second installer when it is opened again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:37:49 -04:00
iris-aiandClaude Opus 5 ded6df559d One notification per thing running, grouped, and an em dash between the two names
Two builds at once were two lines inside one notification; they are now
two notifications in one group, each with its own title and its own bar,
which is what the shade is for -- and what lets a component that
finishes take its own row down while its sibling carries on. Rows are
keyed by a tag (<key>/<component>) rather than an id, so an update
replaces the row it is about and two components cannot collide, and
`drawn` is the path out for one whose work is over.

A foreground service needs one notification that outlives every row, so
the service's own is the row itself while one thing is running and the
group's summary once there are more. Measured on API 36: a group of one
is collapsed to its header, which is the line of text with the bar left
out of it -- so grouping a single row would cost it the progress it had.
Verified on the emulator through all four states: one row with its
count, two grouped rows with a bar each, the fast one finishing and
leaving the other as a single row again, and everything gone when the
last build lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 20:30:35 -04:00
iris-aiandClaude Opus 5 bb65526230 Show an ongoing notification while the app is working
Everything this app asks for is minutes of the build machine's time
followed by a download, and all of it runs in the list screen's own
coroutines -- so with the app in the background the process is an
ordinary cached one, and Android is free to take it away halfway
through a build. A foreground service is the only way to say otherwise,
and its notification is both what that costs and what it is for: the
work is visible from the shade while it runs, and one tap comes back to
the card that started it.

The work itself stays where it is. WorkNoticeService runs nothing --
it collects a list of what is happening and reposts one notification,
stopping as soon as the list is empty -- and that list is *derived* by
workItems from the same projectStates/componentStates the cards are
drawn from, so there is nothing to forget to post and nothing to forget
to take back. The words are shared rather than written twice:
componentWorkLine and buildLine are now read by the card's own bars and
by the notification alike.

Measured on API 36 rather than assumed: the collapsed row drops the
content text the moment there is a progress bar, so one thing running
puts both the component and what is happening to it in the title
("Test Tablet / tablet: building  14/30"); several get a line each in
the expanded view and an indeterminate bar, there being no honest
single number for two builds at once. With the app at the home screen
the process sits at fg-service-act rather than cached, and the builds
it started landed while it was there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 19:36:03 -04:00
irisandClaude Fable 5.1 013d7116d7 An app's own log: a devlog contract, and a Runtime tab for an APK component
Android forbids one app reading another's logcat, so an APK this server
delivers has had no way to say what it did to somebody holding the phone
and nothing else. It can now expose its own bounded log through a
ContentProvider at `<applicationId>.devlog`, guarded by a permission
declared here; README.md's "An app's own log" is the whole contract, and
any project this server delivers can implement it.

The phone reads that provider while the Runtime tab is open and forwards
what is new into the component's runtime log on this machine, so the tab
renders from the same store a service's does and the history outlives the
phone. `LogKind::Runtime` stays one kind with two sources rather than
growing a third, and this server parses nothing -- what arrives is one
line of text each, appended, exactly as a service's stdout is.

The log button is now unconditional, like the gear beside it: with the
tab able to say which of several reasons there is nothing to read, its
absence was the one thing that could not say anything at all.

Supersedes ai-app posting its ring to ai-server over the tunnel, which
put a phone's lines under the wrong component and only ever worked for
that one project.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 16:43:42 -04:00
irisandClaude Opus 5 b67c70fa2f A component can name the files its build reads elsewhere
Freshness compared the commits under a component's own cwd, which is
right about where its files are and wrong about what its build reads.
tdep-survey's two clients each source one shared scripts/android-sdk-env.sh:
a fix committed there changed what every build does, moved no component's
subtree head, rebuilt nothing, and left every card correctly reporting
"current" while answering a narrower question than the reader was asking.

alsoWatch names the rest. The paths join the component's own in the same
subtree-head and dirty comparisons -- git takes several pathspecs, so it
stays one call each -- and it is part of the acceptance gate like any
other declared field. Declared rather than inferred: which files a build
reads is not knowable from here, and both wrong guesses are expensive.

Raised by the tdep-survey session, which traced why its dioxus client
could never become stale on the serving host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 11:11:07 -04:00
iris 90082bd286 Each component builds on its own, and stops reporting when it is done
A project's components were decoupled everywhere except the one place it
showed: there was a single build slot per project, and a single card state
in the app keyed by project alone. So pressing Update on one client of a
two-client project disabled the other client's button for the length of a
build it shares nothing with, drew this one's progress bar and download
percentage under the other's row, and -- had the button been pressable --
would have been a silent no-op on the server, since a second request while
one was running returned without starting anything.

The slot is now per component. `Inner` has no `building` flag; a
component's own `ComponentRun` with an open `step` is the answer, and
`claim` writes that entry synchronously under the lock the route answers
from, so nothing can read a just-claimed component as idle -- which the
phone would take for a build that had already finished. A failure is
recorded against the component whose command it was rather than in the
project's one error slot, which two components building at once cannot
share.

A pull stays exclusive with everything, because there is one checkout and
it rewrites the files every component builds from. Releasing it and
claiming what it decided to build happen under one lock: a phone polling in
the gap would find a project neither pulling nor building and call the run
over.

The app mirrors the split -- `ProjectState` for the pull and the
project-wide Rebuild, `ComponentState` keyed by component for everything
one component is asked to do. Two hierarchies rather than one keyed by a
pair, so a download has nowhere project-wide to be stored. A component's
failure is drawn in its own row beside the Retry that acts on it, which is
also where a failed service action now reports.

And a finished component shows nothing at all: the elapsed times are gone
from both halves of the wire, and its button simply goes back to being
pressable. A bar, a count and a last line all describe something happening
now, and left up they sit there looking live next to a sibling that
genuinely is.

Verified on the emulator against test-projects/two-clients, which exists
for this: while `tablet` built, its row alone carried the bar and its
button alone was disabled, `phone` stayed pressable and silent, and both
returned to normal with no timing left behind.
2026-09-01 03:09:33 -04:00
iris b0e83059a3 dev-updater: build an app on the machine, install it on the phone
A Rust backend that discovers Android projects under configured roots,
builds one on request, and serves the APK over pinned TLS on a WireGuard
interface; an Android client that lists what is buildable, watches a build,
and installs the result. Enrolment carries the token and the CA, so the
phone trusts exactly the machine that issued it and nothing else.

`AGENTS.md` is the working guide and `README.md` the configuration
reference. The shared tunnel-and-TLS code lives in `vendor/wg-app-link`,
which ai-app uses too.

History before this point was squashed away, and a stale `config.json` went
with it: nothing had read that file since the config moved to RON outside
the checkout, and what it still held was one machine's absolute paths and
the names of projects on it.
2026-08-31 20:31:08 -04:00