Add PWA, CLI daemon, deploy tooling, and full documentation

PWA (pwa/):
- Ephemeral QR pairing and trusted device direct reconnect (no QR re-scan)
- Known devices tab as default; pairRoomName() derives deterministic room
- In-app QR scanner via native BarcodeDetector API
- Multi-file send/receive with offer queue and Accept all
- Auto-download on receipt, real-time send/receive progress bars
- Trusted peer nicknames, rename, forget
- Terminal aesthetic: #080808 bg, #00e87a accent, JetBrains Mono
- manifest.json corrected (SVG icon, theme_color, share_target)
- Apple PWA meta tags for home screen install

CLI (cli/):
- flit send / flit recv: ephemeral one-shot transfer with terminal QR
- flit daemon: persistent receiver for trusted peers from ~/.flit/daemon.toml
  - TOML config: signal_url, turn_url, download_dir, [[peers]]
  - One goroutine per peer, exponential backoff reconnect (2s→30s cap)
  - transport.PairRoomName() and Session.OnDisconnected added
- Anchor/TURN config via FLIT_SIGNAL_URL / FLIT_TURN_URL env vars (no hardcoded URLs)

Deploy:
- build-pwa.sh, deploy-pwa.sh / serve-pwa.sh templates in README (gitignored)
- .gitea/workflows/build.yml: PWA build on v* tag, Gitea release artifact
- pwa/public/config.js gitignored; config.js.example committed
- .env.example for CLI env vars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-07-01 15:34:53 +02:00
parent 5050ad5e79
commit 0b06500b9a
22 changed files with 1554 additions and 204 deletions

View File

@@ -222,14 +222,18 @@ function gatherComplete(pc: RTCPeerConnection): Promise<void> {
export interface FileOfferEvent { peer: string; xid: string; name: string; size: number }
export interface FileProgressEvent { peer: string; xid: string; name: string; received: number; total: number }
export interface FileRecvEvent { peer: string; name: string; size: number; url: string }
export interface FileSendProgressEvent { peer: string; xid: string; name: string; sent: number; total: number }
export interface FileSentEvent { peer: string; xid: string; name: string }
type PeerEvents = {
connected: (verified: boolean) => void
connected: (verified: boolean, peerId: string) => void
status: (state: RTCPeerConnectionState) => void
fileOffer: (e: FileOfferEvent) => void
fileProgress: (e: FileProgressEvent) => void
fileRecv: (e: FileRecvEvent) => void
fileCancelled: (xid: string) => void
fileSendProgress: (e: FileSendProgressEvent) => void
fileSent: (e: FileSentEvent) => void
}
export class PeerConn {
@@ -391,7 +395,7 @@ export class PeerConn {
if (m['type'] === 'hello') {
this.verified = m['id'] === this.peerId && this.peerAuthed
this.events.connected?.(this.verified)
this.events.connected?.(this.verified, this.peerId)
} else if (m['type'] === 'file-offer') {
this.events.fileOffer?.({ peer: this.peerId, xid: m['xid'] as string, name: m['name'] as string, size: m['size'] as number })
} else if (m['type'] === 'file-accept') {
@@ -436,8 +440,14 @@ export class PeerConn {
while (dc.bufferedAmount > 1024 * 1024) await new Promise(r => setTimeout(r, 10))
dc.send(buf.slice(offset, offset + CHUNK))
offset += CHUNK
this.events.fileSendProgress?.({ peer: this.peerId, xid, name: file.name, sent: offset, total: buf.byteLength })
// Yield to the event loop every chunk so the browser can paint
// progress — without this, small files finish in one synchronous
// tick and the UI never shows intermediate state.
await new Promise(r => setTimeout(r, 0))
}
dc.close()
this.events.fileSent?.({ peer: this.peerId, xid, name: file.name })
}
private _dc(obj: object) {