Remove dead pair-code flow end-to-end (client, server, protocol) and
the orphaned PublicRoomPanel/DevicePairingPanel/Pair components that
were no longer reachable from the UI.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Auto-claim on receive so /settings shows transfers others sent you.
Filename decryption stays client-side using the key stored in localStorage
when the share link is opened.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Home is now two sections, no clutter:
- Nearby — tap a device on the same Wi-Fi and the composer takes its
place (drop files, optionally write a note, send). Pair-across-networks
and public-room panels moved off the main page into the footer as a
single discrete link (/pair).
- Share a link — inline WeTransfer-style card. Pick a file, optionally
expand email / password / expiry, upload. Result shows QR + copy link
+ the password to share out-of-band.
The password gate is a server-side access control layer on top of the
existing E2E encryption: scrypt-hashed on create, verified on consume.
The encryption key still lives only in the URL fragment; the password
does not participate in the crypto.
- server: password_hash column (migration 0002), scrypt+timingSafeEqual
verify on /consume, requiresPassword surfaced on the HEAD response.
- web: Composer merges drop zone + text note into one surface; Receive
shows a password prompt when requiresPassword is true and recovers in
place on a wrong attempt.
- responsive: mobile-first paddings, DeviceChip shrinks on narrow widths,
no 2-col grids on the main page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a "Via AnyDrop" flow for senders who need to reach someone not
present on the mesh. The file is sealed client-side (XChaCha20-Poly1305),
uploaded directly to an in-cluster MinIO bucket via a presigned PUT, and
handed off to the recipient as a URL whose fragment carries the key.
The server only ever sees ciphertext, opaque metadata blobs, and sizes.
- server: transfers table (drizzle migration), /api/transfers CRUD +
consume endpoint, presigned PUT/GET via @aws-sdk/client-s3, cleanup
loop that purges expired + exhausted blobs.
- web: @noble/ciphers sealFile/openFile, high-level sendCloud/receive
helpers, CloudSharePanel on Home, /r/:id receive page, /inbox page
for signed-in users (sent + received tabs).
- k8s: MinIO StatefulSet with bucket-init initContainer, S3 env vars
on the server Deployment (credentials pulled from minio-credentials
Secret).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace generic slate/indigo dark theme with a custom editorial
direction: warm paper neutrals, oxblood signal, Fraunces serif
display, Inter body, JetBrains Mono for codes. SVG paper-texture
noise overlay and thin rules across the app.
Refactored: Home, Settings, JoinRoom, Pair, Share, plus every
modal and panel (DropZone, DevicePairingPanel, PublicRoomPanel,
ProfileSetup, TextShareModal, ReceiveDialog, TransferProgress,
PeerList, PeerAvatar).
Also drops three pre-existing Uint8Array/BlobPart strictness
errors so the production build is green again.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pairing:
- Always request fresh code when opening "Mon code" tab
- Clear stale pairCode on modal close/tab switch
- Show errors inline for invalid codes (don't close modal)
- Delete pair code after first use (server)
- Validate code length server-side
Notifications removed:
- Remove all showLocalNotification calls (peer-joined, transfer, text)
- Push setup now runs only once per session (no memory leak)
Other fixes:
- Clear pendingFilesRef on disconnect
- Set transfer status to "transferring" immediately on send start
- Select offline peer when tapping to wake
- Fix file receiver blob restoration race condition (await arrayBuffer)
- Clear selectedPeerId after share-target send
- Add returnValue to beforeunload handler
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
iOS PWA (home screen) has separate localStorage from Safari, so
QR-based pairing breaks. Now pairing uses a 6-character code:
- Device A taps "Appairer" → shows a code like "A7K9XB"
- Device B taps "Appairer" → "Rejoindre" tab → enters the code
- Server resolves the code to the groupId, devices are linked
Codes expire after 5 minutes. Pairing is permanent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Peers (available devices)
2. Pair device + Public link (side by side buttons, modals for details)
3. File drop + Text send (only when a peer is selected)
4. Transfer progress
Removed the always-visible drop zone and join-code input from
PeerList. Pairing and public room now open as overlay modals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a groupId-based pairing system so devices can always see
each other regardless of network. Scan a QR code once from the
other device, and they're permanently linked via a shared group
stored in localStorage. No account, no email — just one-time QR
scan like Bluetooth pairing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The ICE candidate regex was matching the wrong part of the candidate
string. Also, iOS Safari blocks local IP detection via WebRTC (mDNS
obfuscation), so when no peers are found, show a prominent "create
link" button and a join-by-code input for easy cross-network pairing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Offline push-subscribed devices appear dimmed in peer list
- Tap offline peer to send wake push notification
- Skip self-notification (own deviceId excluded)
- iOS/Android share sheet via Web Share Target API
- Online/offline indicator dot on peer avatars
- Web Push API for offline device notifications (VAPID)
- Custom service worker with push + share target handlers
- iOS/Android share sheet support via Web Share Target API
- Dedicated /share page with one-tap send to nearby peer
- Background tab notifications for incoming transfers
- Persistent deviceId per device
- Users set their own device name and optional profile picture
- Profile persisted in localStorage (no account needed)
- Auto-detect device type from user agent (iPhone, Mac, Android...)
- Server uses client-provided profile instead of generating names
- PeerAvatar shows photo or device icon instead of animal emojis
- ProfileSetup modal on first visit + editable from header