39 Commits

Author SHA1 Message Date
ordinarthur
629db72764 chore: ignore .env files
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m17s
Prevents accidentally committing Stripe/other local secrets.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 13:28:28 +02:00
ordinarthur
3aaa319264 feat(web): pricing page + plan section in settings (Phase 3)
- web/src/pages/Pricing.tsx: tier comparison, monthly/yearly toggle,
  stripe checkout CTA
- web/src/pages/Settings.tsx: Plan section shows Free/Pro + Upgrade or
  Manage Subscription (opens Stripe Customer Portal)
- web/src/lib/api.ts: startCheckout(), openBillingPortal() + extended ApiUser
- footer on Home gets a discreet "Pricing →" link
- k8s/secrets.example.yml: documents STRIPE_* env vars
- .gitignore: exclude .env files to prevent leaking credentials

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 13:28:18 +02:00
ordinarthur
dc184c4608 feat(server): stripe billing + plan limits (Phase 3)
- schema: stripe_subscription_id, plan_status, plan_expires_at on users
- lib/plans.ts: Free (2 GB · 7d · 1 dl) vs Pro (20 GB · 90d · 100 dl)
- http/billing.ts: POST /api/billing/{checkout,portal}
- http/webhook.ts: verified Stripe webhook → syncs plan lifecycle
- http/transfers.ts: enforces per-user plan limits instead of hardcoded caps
- http/me.ts: exposes plan + status + planExpiresAt to the client

Ops prerequisite: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET,
STRIPE_PRICE_MONTHLY, STRIPE_PRICE_YEARLY env vars in anydrop-app-secrets.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 13:25:55 +02:00
ordinarthur
35aca309c9 refactor: remove cross-network device pairing
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 48s
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>
2026-04-20 13:18:02 +02:00
ordinarthur
54021f88d8 refactor(design): brighter modern blue ink (#2563EB)
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 37s
Shift ink from Prussian-deep #1E3A8A to the brighter blue-600 #2563EB
(Linear/Notion register). Muted + faint tokens follow into cooler
blue-grays. QR codes and rule-strong border updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 13:01:57 +02:00
ordinarthur
3ed673e215 feat(design): shift ink tokens to deep modern blue
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 43s
All text, borders and QR codes now render in blue-ink on cream paper.
ink #1A1714 → #1E3A8A, ink-muted warm → cool blue-gray, rule-strong
+ QR fgColor updated to match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 12:59:14 +02:00
ordinarthur
f1e0c4e3b4 feat(design): modern blue ink accent + stronger paper grain
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 39s
Shift signal from oxblood (#7A2320) to modern blue (#1D4ED8),
signal-quiet from warm rose to pale blue wash. Bump body grain
opacity 0.35 → 0.6 so the paper texture actually reads.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 12:53:32 +02:00
ordinarthur
2452f2642a feat: inbox of received transfers in /settings
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 47s
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>
2026-04-20 12:47:10 +02:00
ordinarthur
c46b23b8ec feat: shared-links history in /settings with revoke + copy
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 39s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 12:26:04 +02:00
ordinarthur
bdfa11a2bf feat: paper-grain favicon + PWA icons in modern blue
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 39s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 12:18:02 +02:00
ordinarthur
dbd500b0b5 feat: simpler Home + password-protected cloud links
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 51s
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>
2026-04-20 11:54:05 +02:00
ordinarthur
6ba5401c4d chore(infra): point cloud relay at shared cluster MinIO
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m11s
Drop the standalone MinIO StatefulSet — the cluster already runs one
in the `minio` namespace, exposed at minio.arthurbarre.fr. Use that
with a scoped anydrop user + bucket instead of spinning up a second
instance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 11:25:19 +02:00
ordinarthur
0b639dfc3c feat: encrypted cloud relay (Phase 2)
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>
2026-04-20 11:09:58 +02:00
ordinarthur
c18d995c3f feat(web): Paper & Envelope design system
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>
2026-04-20 10:49:15 +02:00
ordinarthur
3f87debcf8 fix(maddy): correct global hostname + run subcommand + dkim syntax
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 37s
- add top-level `hostname $(hostname)` directive (required by maddy 0.8)
- invoke as `maddy -config … run` (global flag before subcommand)
- fix dkim syntax: `dkim DOMAIN SELECTOR` (no key-size positional arg —
  that was being parsed as a second selector, generating bogus keys)
- use bounce block on local_queue target instead of (local_routing) macro
2026-04-20 10:18:58 +02:00
ordinarthur
ef2725aebf fix(build): stop committing tsbuildinfo, wipe before tsc on docker
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m21s
Root cause of the CI "Cannot find module @anydrop/shared" error:
server/tsconfig.tsbuildinfo and shared/tsconfig.tsbuildinfo were checked in.
On the Gitea runner, tsc read those files, concluded the prior (local-machine)
build was still valid, and skipped emitting shared/dist — so there was nothing
to resolve by the time server's tsc ran.

- untrack the two .tsbuildinfo files
- gitignore *.tsbuildinfo
- dockerignore **/dist, **/*.tsbuildinfo (belt-and-suspenders)
- in both Dockerfiles, delete any stray tsbuildinfo + dist dirs before tsc
2026-04-20 10:11:00 +02:00
ordinarthur
0b90d9cdb5 fix(docker): materialize @anydrop/shared as real dir, not workspace symlink
Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 11s
pnpm's workspace symlink (server/node_modules/@anydrop/shared → ../../../shared)
works locally but breaks on the Gitea Actions runner. TSC resolves the symlink
but cannot read through it, yielding TS2307 on "@anydrop/shared".

Fix: after building shared, copy its package.json + dist into
{server,web}/node_modules/@anydrop/shared as a plain directory before running
the dependent build. Module resolution becomes filesystem-local, independent
of BuildKit layer semantics or storage driver symlink handling.
2026-04-20 10:05:47 +02:00
ordinarthur
2913618ee6 feat: stealth accounts + data layer (Phase 1)
Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 1m47s
2026-04-20 09:57:22 +02:00
ordinarthur
acda1a8bb8 feat: auto-update PWA when new version available
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 35s
Uses registerSW to detect new service worker versions and reload
automatically. Defers reload if file transfers are active, checking
every 30s until transfers complete.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 13:27:40 +02:00
ordinarthur
dd37b49ce4 fix: pairing flow + code review bugfixes + remove notifications
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 45s
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>
2026-04-14 13:22:33 +02:00
ordinarthur
b407e6ce95 fix: pairing reconnect sends stale groupId
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 37s
SignalingClient captured the profile at construction time, so
after pair-code-resolved updated the store, the reconnect still
sent the old groupId. Added updateProfile() method and call it
before reconnecting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 13:10:36 +02:00
ordinarthur
d8721b6d0c feat: pairing via short code instead of QR only
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 45s
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>
2026-04-14 13:04:58 +02:00
ordinarthur
f23cb7bffc fix: explicit MIME types for iOS share target
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 38s
iOS Safari ignores wildcard */* in share_target accept.
List specific MIME types and file extensions so AnyDrop
appears in the iOS share sheet for photos, videos, docs, etc.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 12:58:44 +02:00
ordinarthur
62aeb895c1 perf: 16x larger chunks + native backpressure for faster transfers
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 41s
- CHUNK_SIZE: 16 KB → 256 KB (16x fewer send calls)
- MAX_BUFFER: 1 MB → 4 MB (more data in flight)
- waitForDrain: polling setTimeout → native bufferedamountlow event

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 12:53:36 +02:00
ordinarthur
8212bc7391 refactor: simplify layout flow
Some checks failed
Build & Deploy / build-and-deploy (push) Has been cancelled
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>
2026-04-14 12:52:54 +02:00
ordinarthur
612222ccde feat: transfer resume after page refresh
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 43s
Transfers now survive page refreshes:
- beforeunload warning prevents accidental refresh during transfer
- Sender's files cached in IndexedDB before sending
- Receiver's chunks persisted in IndexedDB as they arrive
- On reconnect, receiver sends transfer-resume with bytes received
- Sender resumes from offset, skipping already-sent data
- Old transfer data auto-cleaned after 1 hour

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 12:45:05 +02:00
ordinarthur
d6f7a2374b feat: permanent device pairing for cross-network sharing
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 43s
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>
2026-04-14 12:39:21 +02:00
ordinarthur
2e3408e8d7 fix: local IP detection regex + add join code UI when no peers found
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 38s
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>
2026-04-14 12:34:01 +02:00
ordinarthur
99a182a831 feat: LAN detection fallback via WebRTC local IP
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 41s
iCloud Private Relay hides iPhones' real public IP, breaking
IP-based LAN grouping. Devices now detect their local IP
(192.168.x.x) via WebRTC ICE candidates and send it to the
server. The server groups devices by local subnet as a fallback
when public IPs differ.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 12:27:36 +02:00
ordinarthur
d1c4b3d196 feat: offline peers visible + push wake + iOS share sheet + no self-notification
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 44s
- 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
2026-04-14 12:12:08 +02:00
ordinarthur
0034e91672 feat: push notifications + iOS share sheet integration
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 37s
- 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
2026-04-14 12:05:59 +02:00
ordinarthur
fd249abbf1 feat: push notifications + background transfer alerts
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m1s
- Web Push API for offline device notifications
- Custom service worker with push event handling
- Local notifications for background tab transfers
- VAPID keys in K8s config
- Persistent deviceId per device
2026-04-14 12:03:43 +02:00
ordinarthur
d8d747276a fix: gate signaling behind profile setup, fix SW cache
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 37s
2026-04-14 11:55:51 +02:00
ordinarthur
e3085ea7a2 feat: local device profile replaces animal naming
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 35s
- Users set their own device name and optional profile photo
- Profile persisted in localStorage, no account needed
- Auto-detect device type from user agent
- Server validates client-provided profile defensively
- PeerAvatar shows photo or device icon
- ProfileSetup modal on first visit, editable from header
2026-04-14 11:53:03 +02:00
ordinarthur
4dbdfedae0 feat: local device profile (name + photo) replaces animal naming
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 43s
- 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
2026-04-14 11:47:38 +02:00
ordinarthur
953f3cb8a1 ci: retry after worker reboot
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 3m2s
2026-04-14 10:52:49 +02:00
ordinarthur
94339a1cb2 ci: retry after docker dns fix
Some checks are pending
Build & Deploy / build-and-deploy (push) Waiting to run
2026-04-14 10:44:44 +02:00
ordinarthur
fea93dc121 ci: trigger first deployment
Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 10s
2026-04-14 10:39:24 +02:00
ordinarthur
9d6e4da4ae feat: initial commit with full deployment setup
Some checks failed
Build & Deploy / build-and-deploy (push) Failing after 8s
Includes React PWA frontend, WebSocket signaling server, shared types,
K8s manifests, Gitea CI/CD workflow, nginx config, and Dockerfiles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 10:30:45 +02:00