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:
@@ -48,6 +48,16 @@ func NetHash(name string) string {
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
// PairRoomName derives a stable room name for two known peers — mirrors
|
||||
// pairRoomName() in pwa/src/pairing/ephemeral.ts. Both sides compute it
|
||||
// independently from their stored peer IDs, so no QR/invite is needed.
|
||||
func PairRoomName(a, b string) string {
|
||||
if a > b {
|
||||
a, b = b, a
|
||||
}
|
||||
return "flit-pair:" + a + ":" + b
|
||||
}
|
||||
|
||||
func iceServers(cfg Config) []webrtc.ICEServer {
|
||||
servers := []webrtc.ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}
|
||||
if cfg.TurnURL != "" && cfg.TurnSecret != "" {
|
||||
@@ -148,10 +158,11 @@ type Session struct {
|
||||
sig *signaling
|
||||
pendingRecv *recvState
|
||||
|
||||
OnConnected func(verified bool)
|
||||
OnFileOffer func(FileOffer)
|
||||
OnConnected func(verified bool)
|
||||
OnDisconnected func()
|
||||
OnFileOffer func(FileOffer)
|
||||
OnFileProgress func(xid string, received, total int64)
|
||||
OnFileDone func(name string, path string)
|
||||
OnFileDone func(name string, path string)
|
||||
}
|
||||
|
||||
func NewSession(dataDir string, cfg Config) (*Session, error) {
|
||||
@@ -229,6 +240,21 @@ func (s *Session) connectTo(ctx context.Context, peerID string) error {
|
||||
s.mu.Unlock()
|
||||
|
||||
pc.OnDataChannel(func(dc *webrtc.DataChannel) { s.wireDC(dc) })
|
||||
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
||||
if state == webrtc.PeerConnectionStateFailed ||
|
||||
state == webrtc.PeerConnectionStateDisconnected ||
|
||||
state == webrtc.PeerConnectionStateClosed {
|
||||
s.mu.Lock()
|
||||
s.pc = nil
|
||||
s.ekeySent = false
|
||||
s.offered = false
|
||||
s.peerEPK = nil
|
||||
s.mu.Unlock()
|
||||
if s.OnDisconnected != nil {
|
||||
s.OnDisconnected()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
s.sendEkey()
|
||||
offerByOrder := s.identity.PeerID() < peerID
|
||||
|
||||
Reference in New Issue
Block a user