Skip to content

Milestone/docs and fixes - #1

Merged
k1nz merged 19 commits into
mainfrom
milestone/docs-and-fixes
Sep 10, 2026
Merged

k1nz merged 19 commits into
mainfrom
milestone/docs-and-fixes

Conversation

@k1nz

@k1nz k1nz commented Sep 10, 2026

Copy link
Copy Markdown
Owner

No description provided.

k1nz added 19 commits September 10, 2026 15:46
The action bar rendered "↵" and a lone ⌘ glyph as plain text, which fell
back to whatever font the system picked and sat on the baseline rather
than reading as a key. Split shortcut strings into parts so each key can
be drawn as its own cap, and reuse that in the actions menu and the
shortcut recorder.

formatShortcut now spells keys out ("Command Shift V") because the
recorder button exposes it as an aria-label.
Add an overlay walkthrough and a pairing preview section, and rework the
hero, types, and privacy sections around stills of the real windows. Key
combinations now render with the same keycap treatment the app uses.

DownloadSection gains a first-launch note, which is where the unsigned
build's "Open Anyway" instructions belong.
macOS 15 gates local network access behind a user permission. Without
NSLocalNetworkUsageDescription and the Bonjour service type declared in
the bundle's Info.plist the prompt has nothing to show and discovery can
fail with no visible error — and discovery is the entry point to pairing,
since a peer that is never found can never be added.

Tauri merges src-tauri/Info.plist into the plist it generates.
The fallback URLs pinned v0.1.0's filenames, so the next release would have
left them pointing at an old version — and they are the only path when the
GitHub releases API is rate-limited or blocked.

Resolve the version at runtime instead: fall back to the version published
in the raw package.json manifest, which is a different host and not
throttled, and pair it with the releases/latest permalink. Only when
GitHub's API hosts are both unreachable does a pinned URL get used, and
that one is pinned to a release tag so it stays downloadable instead of
404ing the moment a newer release renames the assets.

Platform detection read userAgentData.architecture, which only Chromium
exposes: Safari on an Intel Mac was offered the Apple Silicon build, and
Linux visitors were offered a macOS one. An architecture that cannot be
determined now offers both Mac builds rather than guessing arm64.
The footer and both locale files say MIT, but the repository carried no
LICENSE file, so it granted nothing.
The backend reports a pinned-certificate mismatch as `trust_broken` and the
device is marked accordingly, but no rule matched it, so the user only ever
saw the generic error where the spec asks for "trust failed, pair again".

The disk rule was the opposite problem: the wording existed but no code path
could produce it, so it was a message that could never be shown.
A failed list fetch went through invokeSafe, which returned null, so a
history or device load that errored rendered exactly like an empty one —
the user could not tell "nothing to show" from "the backend is down". List
loads now report failure separately from emptiness and the existing banner
and error states show it, with a loading state so an empty list is only
claimed once the fetch has actually finished.

Keyboard fixes: the actions menu index incremented past the end of the list,
after which Enter did nothing; the type filter could only be clicked; and
the listbox claimed aria-activedescendant on a container that never held
focus, so screen readers could not follow the selection. The replacement
puts the active-descendant on the focused search field.

Delete is now a two-step confirm inside the menu rather than an immediate,
unrecoverable action, and offline devices are selectable instead of
disabled, so the failure is reported instead of being prevented — which is
what the spec asks for.

HTML items preview as text rather than raw source: the markup is parsed
inertly with DOMParser and reduced to readable text. Rendering it would
need a sanitiser, which this app does not ship.
The rule looked for "source gone", but the backend returns
`source_file_gone`, so a paste whose source file had been cleaned up showed
the generic message instead of saying the other machine no longer has it.
Found by the test added alongside it, which asserts the real wire strings
rather than the message wording.
The frontend had no tests at all: every check was a type check, so nothing
verified behaviour. Add vitest and cover the modules that are pure logic —
byte and time formatting, shortcut parsing, history grouping, the error
mapping, and title labelling — including the bilingual catalogue key-parity
rule, which until now was only asserted on the Rust side.

Tests assert the strings the backend really sends, which is what caught the
mismatched source-gone rule fixed in the previous commit.
FlatRow is a discriminated union, so the label only exists on the header
variant. vitest does not type check, so this only surfaced in the build.
About pane printed "0.1.0" written into the component, and the release
workflow's version gate only checked package.json, tauri.conf.json and
Cargo.toml — so the string would have drifted the first time a release
bumped the other three. Inject the version from package.json at build time
so there is no fourth copy to forget.
The conventions that are easy to get wrong are not obvious from the code:
UI work is bound by design-system/lanpaste, every user-visible string needs a
key in both catalogues, and backend errors are codes that must be mapped on
the frontend. Write them down, along with how to build, test and release.

The bug template asks the things that actually decide most reports — both
machines' operating systems, whether they are on the same network, and
whether discovery or the transfer is the part that fails.
The root `site` script invoked pnpm while site/ carries an npm lockfile and
CI installs with `npm ci`, so the convenience script only worked for someone
who happened to have pnpm installed. Also add a `test:rust` script so the
Rust suite runs the same way as the frontend one.
Nothing ran the test suites: the frontend had no runner at all, and the Rust
tests existed but only ever ran on a developer's machine. Add a workflow
covering type checking, tests and locale parity for the app, a build of the
marketing site, and `cargo test` on macOS, which is the platform the
clipboard layer and the golden path are built for.

Deliberately no `cargo fmt --check` and no clippy gate: the crate is not
rustfmt clean today (13 files differ), so either would fail on its first
run. Both are worth adding once formatting is settled in its own change.
Three claims no longer matched the code. The overlay is no longer macOS
only, so the north star should not read as a macOS-only target. The sticky
time-section label was dropped by design and was never implemented, so
listing it as a requirement invites someone to "fix" it back. And the
checklist named only the macOS chord.

Also add the constraint the app already follows but the design system never
stated: user-visible copy comes from the locale catalogues, both of which
must carry the same keys, and a design doc should cite the key rather than
a literal string in one language.
The spec still described a macOS-only v1 with a Cmd+Shift+V default and
Simplified-Chinese-only copy; Windows shipped, the default is Option+Shift+V
on macOS and Ctrl+Shift+V elsewhere, and the UI is bilingual and follows the
system language. Fix those in place, and add a short section recording what
landed that the spec never anticipated, so none of it gets mistaken later
for missing work.

The implementation contract had drifted in smaller ways: the pairing input
window is 400x220 rather than 400x200, a tray click toggles the overlay
instead of only showing it, and the event list was missing locale-changed.
Note at the top that the code wins where the two disagree.
The shortcut sections were already right, but a reader had no link to the
published site, and "Building from source" stopped at the dev server. Add
the download page, the production build command, and a line that the site
under site/ is built separately. Both languages get the same changes.
Four problems, all around what the network layer promises.

Transfers could not resume. The server has always answered Range requests,
but the client asked for the whole file, buffered it in memory, and threw
every received byte away on failure — so a large file on a flaky link could
never finish. The client now streams to a partial file beside the blob
store, retries with `Range` plus `If-Range` so a changed file is never
spliced onto stale bytes, and only moves the bytes into place once the whole
file has arrived. A partial that already holds the full length (the process
died between the last byte and the commit) is finalized without touching the
network. Progress is reported cumulatively so the bar does not jump back on
resume, and per-item locking keeps two downloads of the same file from
interleaving into one partial.

A blob could be deleted mid-transfer. The download handler marked a blob
in-flight and then cleared the mark from a task that slept two seconds,
regardless of whether the stream had finished — so any transfer longer than
two seconds, or two concurrent downloads of one blob, could have its bytes
garbage-collected underneath it. The mark is now held by the response
stream itself and released when the stream ends, the client disconnects, or
the response drops.

The retry queue both leaked and stalled. A send that failed while the peer
was reachable — rejected, over the size cap, any HTTP error — was dropped
silently, and the caller ignored the error. Failures are now classified:
transient ones are queued, permanent ones are returned so the UI can show
them. The queue also records attempts and the last error in new columns that
migrate existing databases in place, and it evicts rows that can never
succeed instead of retrying them every three seconds forever.

Three endpoints trusted more than they should. `POST /pair/revoke` had no
authentication at all, so any host on the network could unpair someone
else's machines; it now requires a signed request and a device may only
revoke itself. `GET /files/:id` skipped its token check whenever an item had
no token, and image items never got one, leaving those blobs readable by any
paired device; every downloadable item now carries a token and the token is
always required. And `pair_confirm` accepted whatever key, certificate and
fingerprint the caller supplied, never comparing them to what the pairing
request had declared — it now cross-checks every field and rejects
mismatched or malformed claims.
Rejecting a confirm whose identity claims do not match the request
introduced a new code, and it is one a user can hit while pairing, so it
needs wording rather than the generic failure message.
@k1nz
k1nz merged commit 21db33c into main Sep 10, 2026
3 checks passed
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.

1 participant