Skip to content

monorepo finale: relocate crates, monitor release pipeline, docs overhaul - #1234

Draft
lollipopkit wants to merge 265 commits into
mainfrom
frb
Draft

monorepo finale: relocate crates, monitor release pipeline, docs overhaul#1234
lollipopkit wants to merge 265 commits into
mainfrom
frb

Conversation

@lollipopkit

@lollipopkit lollipopkit commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

This PR finishes the monorepo migration with three main blocks, plus follow-up hardening:

1. migrate: merge rust/ and rust_builder/ into crates/sbm_ffi

  • The cargokit plugin shell and the sbm_ffi crate now share one directory (cargokit's official example layout), keeping the top level clean
  • Workspace members, FRB rust_root, pubspec paths, and per-platform glue updated in sync
  • Verified: cargo test --workspace green, 12/12 FFI parity tests pass, flutter build macos --debug builds through cargokit for real

2. feat: monitor release pipeline

  • Manual trigger only; version comes from the monitor crate (monitor-v* tags), decoupled from the app's v1.0.x
  • Native binaries: Linux musl (amd64/arm64, native arm runners) / macOS (both archs) / Windows → GitHub Release + SHA256SUMS
  • Docker: the Linux musl binary is reused in an Alpine image pushed to ghcr.io — zero in-image compilation, no QEMU
  • Drive-by fixes: Dockerfile frontend path mismatch with server.rs, build/runtime glibc mismatch, reqwest switched to rustls to drop the OpenSSL dependency (prerequisite for static musl builds)
  • install.sh rewritten for the new release layout; fresh installs generate a random JWT_SECRET
  • Verified: local image build + run smoke test (health 200, frontend OK, uptime collection OK), 41 monitor tests green

3. docs: full audit of the docs site (en/zh/ja/de/fr/es)

  • Removed content confirmed fabricated after checking against the code: system tray, Touch Bar, multi-window, NUL keep-alive, SFTP paging/TTL cache, nonexistent keyboard settings, etc.
  • Fixed factual errors: home tab list, Windows integration language, encrypted-storage description, class/setting names, nonexistent test file examples
  • Added monorepo layout, Rust testing, and FRB codegen docs; README monitor links point into the monorepo
  • Added dependabot config (cargo × 3 directories + monitor frontend npm + GitHub Actions)
  • Removed doc/adr (intentional) and cleaned up all references
  • Verified: docs site builds, 139 pages

4. Follow-ups on this branch

  • Monitor dependencies upgraded across major versions (backend: ntex 3, sqlx 0.9, jsonwebtoken 10, reqwest 0.13, etc.; frontend: react 19, vite 8, vitest 4, tailwind 4, eslint 10) — also resolves the vitest critical CVE (GHSA-5xrq-8626-4rwp) flagged by Socket
  • All 6 inline review findings fixed (full-volume drop on Windows, missing Windows net counters, hard-coded velocity key, unbounded retention tables, dead serve CLI flags + TLS wiring, since-boot CPU average)
  • Security hardening: no baked-in admin/admin123, no default JWT secret — both generated randomly on first start

Notes

  • After the first monitor-release workflow run, set the server-box-monitor ghcr image to public in package settings if desired
  • The cargokit path changes for iOS/Android/Linux/Windows mirror macOS but are unverified on-device; watch the first five-platform CI build

Summary

Changes

  • Workspace and FFI build pipeline: Rust workspace, Flutter FFI plugin integration, generated bindings, and cross-platform native build tooling are introduced or reconfigured.
  • Shared parser and script contract: The shared Rust parser, platform command manifest, script generator, data types, fixtures, compatibility tests, and SSH/e2e coverage replace the former Dart parsing/script implementations.
  • Flutter FFI and monitor client migration: The Flutter application migrates status collection to Rust FFI and adds monitor HTTP credentials, metrics models, providers, and integration paths while removing old Dart parser/script code.
  • Native sampling and monitoring state: A native sampling crate and monitor collection loop add local OS sampling, extended-cycle scheduling, carry-forward state, GPU probing, metric adaptation, and historical storage.
  • Monitor configuration and database lifecycle: Monitor configuration loading, environment/CLI overrides, JWT secret lifecycle, database bootstrap, migrations, cleanup, and persistence contracts are added or changed.
  • Monitor API, auth, and runtime configuration: The monitor HTTP API adds login, JWT-protected metrics/capabilities/history/velocity/settings/card-order endpoints, compatibility status, TLS, CORS, and runtime settings updates.
  • Monitor web frontend and shared UI: The new Svelte monitor frontend, shared web UI package, generated/localized strings, server registry, polling, dashboard cards, charts, settings, and multi-server flows are introduced.
  • Packaging, release, and documentation: Release packaging, installation, Docker/Compose assets, workflow automation, repository guidance, README/TODO content, documentation translations, and removal/renaming of the old monitor package are changed.

The way out was the way in played backwards, so it reuses the same machinery:
capture where the thing is now, let the layout change, then measure where it
landed and move a copy between the two.

One key marks both ends. The compact row and the grid card can never be on
screen together — the row exists only while the pane is open, the card only
while it is closed — so a single anchor is unambiguous, and one `GlobalKey`
with one holder is worth more here than two that each need watching.

The row carries the anchor while merely selected, not only while flying:
closing has to measure it *before* the rebuild that takes the pane away, and
by then there is nothing left to ask.
Each direction now says what it is leaving as well as what it becomes: a card
with its charts on one side, a card with only its title on the other. The row
form is also what a card of a server that has never connected looks like,
which is why the two read as one thing changing shape rather than two widgets
swapped for each other.
`restorationId: 'tab_$id'` is interpolated where it is written, and where it
is written is inside the callback `add` runs to build the session — before
`add` has returned the tab whose id that is. Every attempt to open a terminal
threw `LateInitializationError`.

The tab's name works and is in scope there: unique among the open tabs, and
restored in the same order, which is all a restoration key needs. The id stays
for the close callback, which is only ever called later.
The per-tab navigator stack survives switching tabs, which had been marked as
reasoning rather than something anything had run.
`enabled` read the setting once per build and nothing brought this page back
to ask again — the setting lives on another page, and no provider carries it.
So it applied whenever something unrelated happened to rebuild, which from the
outside is indistinguishable from not working.

Listened to now, the way the text-scale setting on this same page already is.
Two failures found on the way: the single-column setting was read once instead
of listened to, and a second window caption was drawn on every page pushed
inside a tab.
The tab strip sat above the local file page's own app bar, so the Files tab
opened with two rows of chrome and half the height gone before any file.

The local page now hands its buttons to the host through `actionsSink`, the
same way the SFTP page already did, and draws no bar of its own when it has
somewhere to hand them. Its title went with the bar, so it reports the folder
it is showing as well, and the strip's leading tab carries that name beside
its icon.

Pushed on its own — the file picker — the page is unchanged and still
self-contained.
This device was pinned to the head of the strip and servers were the only
thing that could be a tab, so the two were not the same kind of thing even
though browsing either is the same act. Nothing could hold two folders on this
device at once either.

The leading tab is now a picker listing this device and every server, and both
open as ordinary tabs. `_FileSession` is sealed over the two: they differ in
what it takes to reach them and in nothing else a tab cares about.

Restored state tells them apart by the absence of `serverId`, which every
record written before this carries, so tabs saved by an older build come back
as the server tabs they were.
Both pages now read like the server list: the picker on the left, what it
opens on the right, one draggable divider between them, and the same
`forceSinglePane` setting and stored width.

The picker is the first tab only while there is no column for it. Its width
threshold also requires something to be open, so a fresh launch spends the
whole window on the picker rather than reserving a column beside nothing.

The SSH add-server button moved onto the picker itself, which is where it
acts. It used to hang off the tab page and check the selected index to decide
whether to show itself; now it is simply part of the page it belongs to, and
follows that page into the column.
On a wide screen both pages now show the rail on the left and, on the right, a
plain bar naming the session that is on screen with that session's own buttons
beside it. The strip stays on one screen, where there is no rail.

The rail is not the picker in a narrower column. A grid of cards is for
browsing and choosing; a rail is for switching while something else has your
attention, and it carries the running sessions the picker has no business
knowing about. Tapping a server in it always starts a new session — the
section above is where switching happens, and a second shell on one server is
an ordinary thing to want.

For files, this device sits above the first heading rather than under one of
its own: it is the place that is always reachable, not one entry among many.

SSH gets an add-server button in the rail's toolbar, since a rail has no room
for the picker's floating one.
It was a column of cards, each carrying the title row a card shows before it
connects. Beside a detail pane that is a smaller copy of what the pane already
draws, and it read as a third way of listing the same servers the terminal and
file rails list.

Now it is `SideBarTile`: the name, the fill on the one being shown, and the
live mark on the ones that are connected — the same mark the terminal rail
puts on a running shell.

The flight lands on that row instead of a card, drawn selected, since a card
only ever flies into the rail as the thing that was just opened.

The rails elsewhere drop their addresses too. The narrow pickers keep theirs:
those are cards at full width, where the second line costs nothing.
The server grid had its own copy of the round-robin split — a `ListView` per
column, each scrolling on its own. Server cards are the worst case for it: one
that has not connected is a single line and one that has is several charts, so
the columns ended up wildly uneven. They now flow into whichever column is
shortest, in one scrollable, built as they come into view so the page watches
the servers it is showing rather than all of them.

`PageColumns` keeps its ceiling of three columns; the width it caps itself at
is now derived from the grid's own arithmetic instead of a second copy of it.

The pickers drop the spacing they had stacked up: one gap between two cards
rather than a column gap, a scrollbar gutter and two card margins.
Terminal, SFTP, container and the rest acted on the server, not on any one
card, but they sat at the top of the page as if they were its first card — so
they scrolled away after the first chart and getting back to them meant
scrolling to the top.

They are now a bar over the page, reachable the whole way down, that slides
out of the way while you read forward and comes back the moment you scroll
back. The grid keeps room below its last card, so nothing ends up permanently
under it.

The row centres itself where it fits rather than being left-aligned in a
container the width of the window, and scrolls where it does not.
A monitor-only server has one button, and the bar was still the full 640 it
was capped at, with a lone Terminal floating in the middle of it.

The row is now a `Wrap`, which reports the sum of its children as its
intrinsic width, so an `IntrinsicWidth` above it can size the bar to what is
actually in it — the cap only applies once there are enough buttons to reach
it. The bar's height follows its contents too, and a language whose labels do
not fit takes a second line rather than being clipped.

The in-card row keeps the scrolling form: it is at the card's width either
way, and there it is the width that is given rather than asked for.
They sat directly against each other and against the bar's edge, so seven
buttons read as one block rather than as seven things to choose between. The
outer inset is larger than the gap between them, which is what makes the row
read as a group inside a border rather than as buttons that happen to be near
an edge.
Three changes to the same row. It is a single line now, scrolling where it
does not fit, instead of wrapping to a second one. Its ceiling is the page's
width less 100, so there is always some of the page visible either side of it
and it never reads as a second edge to the window. And 3 above and below the
buttons rather than 14 — the buttons bring their own tap target, so the
padding only has to keep them off the border.

A shrink-wrapping viewport replaces the `Wrap`: it takes the width of what is
in it and no more, and scrolls once whatever holds it runs out of room, so the
bar still fits itself to one button without needing the intrinsic width the
`Wrap` was there to report.

The buttons' tap target comes down from the default 48 square, which in a bar
this height was all of it.
A host reports as many mounts as it has, and most of them are loop devices and
container layers. The throughput chart sat after that list, so on a real
machine it was never seen; the mounts are now behind a disclosure that starts
closed, with the chart above it — which is what the network card already does
with its interfaces, for the same reason.

Charts get room above them: at 7 the topmost axis label touched whatever
heading was over it.

The action bar keeps less space under its labels than over its icons. A line
of text carries its own space beneath it, so equal padding reads as more.
An `IconButton` with a `Text` under it left the word inert, so half of what
reads as one target did nothing. The label is now inside the button rather
than captioning it, and the ink covers both.

The bar is 3 shorter for it, and carries the height it needs for a 17pt icon
over a line of 11pt text — at 52 the column overflowed it by a pixel.
It was a row in the SSH settings, which is where someone goes to change how
terminals look, not to find a machine. Sweeping the network for hosts is what
you do with an empty add-server form in front of you, so it is an action on
that page's bar — and only while adding: on a server that already exists it
answers a question nobody is asking.

The behaviour is unchanged: it still adds every host picked, under one user
typed once, rather than filling in the form. Discovery finds hosts, not
accounts, and someone who went looking at their whole network usually wants
more than one of what they found. The form is left alone for the server they
were adding by hand.
The switch existed because neither place was good: on the card they crowded a
grid of charts, and at the top of the detail page they scrolled away. They
float over the detail page now, in reach on both layouts, so the question the
setting asked no longer has two answers.

Gone with it: the branch in `ServerFuncBtns` that drew a bare icon row for the
card, the card's own row and the taller card height that made room for it, and
`moveOutServerFuncBtnsHelp` in fifteen languages. The stored key stays until a
release has passed, marked deprecated with a TODO.

The settings entry is a single row again — the order of the buttons — instead
of a disclosure holding one switch and one row.
Reached from the add-server page, a full-screen sweep that ended by adding
servers left the form it was opened from sitting there untouched — the one
thing on screen it should have had something to say to.

It is a dialog over that form now, and picking a host writes into it: the
address and the port, and the name as well while that field is still empty,
where the address is what someone would have typed anyway. Nothing else — a
sweep finds hosts, not accounts, so there is no user, key or anything else to
guess at. The banner stays on screen as the only way to tell two addresses
apart, and goes nowhere: it names an SSH build, not a machine.

One host at a time, so the multi-select and its bulk import go, along with
`isSelected` on the model, kept one release for old persisted reports. The
files move out of `view/page` — none of this is a page any more.
It said "tap the search button to discover SSH servers on your network". On a
page that meant the labelled one filling the bottom corner; in a dialog it
meant an 18pt icon in the top one, which is not something a line of grey text
can point at.

The empty view is the button now — labelled, in the middle, where the sentence
was. The icon in the bar appears only once there are results for it to
replace, so there is never more than one way to start the same sweep.

`tapToStartDiscovery` goes in fifteen languages: it was directions to a
control, which is what a control that can be seen does not need.
Resolves six conflicts, all where an upstream change landed on code this
branch had already rewritten:

- iOS Live Activity cleanup (#1265) turned `stop()` async; the accessory
  widget URL channel this branch added keeps its case.
- Combined key+password auth (#1270) drops the either/or between the two.
  Applied through `spi.ssh` rather than the flat fields it was written
  against, and extended to the SSH-via-monitor credential, which is the
  same question asked about a second account.
- The global Agent workspace (#1268) attached its terminal entry point to
  the tab strip this branch replaced with a rail; the button moves to the
  rail's session actions. Its status JSON reported a CPU with no sampling
  window as 0% — that field is nullable here precisely because a
  fabricated 0 reads as a genuinely idle machine, so it now reports null.
`raw.split(separator).isEmpty` is never true — `String.split` returns at least
one element — so output the script never wrote passed the check and reached
the parser, which produced an empty status. The page then kept whatever its
rolling state still held (cpu, network speeds) and blanked the rest, with
nothing on screen saying why.

That is exactly what a host answers once the script is gone from under it, and
the default script directory is /tmp. Reported as a failure, the connection
goes back through the connect path, which reinstalls it.

The check is now for the marker itself, and accepts either — a custom command
carries its own, and with every built-in status command disabled it is the
only output there is. The message distinguishes nothing at all from something
unparseable, and quotes it.
The code encodes the whole record — address, user, password, key id — as
plain text. `QrShareBtn` put it straight on screen, so a code that hands over
a server to anyone who photographs it looked like any other share button.

It now opens a dialog that says so above the code. The width is capped rather
than fixed: the QR fills whatever it is given and the warning is one long
line, so on a desktop window their intrinsic widths would have made a QR code
the height of the screen, while a narrow phone should still get less.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants