Replace the "drop a session token in a file" workflow with a real
Chrome-style email login. The Cloudflare worker already had Better
Auth's `email-otp` plugin wired into `SEND_EMAIL`; this commit
builds the renderer-side counterpart.
Worker side:
- Move the OTP sender from `auth@elydora.com` to `browser@elydora.com`
(wrangler.toml `allowed_sender_addresses` + better_auth.ts
`EMAIL_OTP_FROM_ADDRESS`). Worker must be redeployed to pick this up.
Client side (`ely_sync_client::email_otp`):
- `send_email_otp(config, email)` POSTs `/api/auth/email-otp/send-verification-otp`
with `{ email, type: "sign-in" }`.
- `verify_email_otp(config, email, otp)` POSTs `/api/auth/sign-in/email-otp`,
reads the Better Auth session token from the JSON body's `token` field
with the `Set-Cookie: better-auth.session_token=…` header as the
documented fallback channel, and returns it as a `BearerToken`.
Shell side (`shell/auth.rs` + `shell/internal_pages/sync.rs`):
- New `AuthFlowPhase` (Idle / SendingCode / AwaitingOtp / Verifying /
Error) tracks the in-flight form. Two off-thread workers run the
HTTP exchanges so the GPUI render loop never blocks.
- Successful verify saves the bearer via `SyncEngine::install_bearer`
and triggers an immediate snapshot upload, so the user is signed in
+ initial-synced in one click.
- Sync settings page replaces the bare "Sync now" button row with an
account card: when SignedOut → email field + Send code → OTP field
+ Verify / Resend; when signed in → an account chip + Sign out.
- `trigger_cloud_sync_upload` no longer takes a `Context` param so
the post-auth path can fire it from the inbox-drain pass without
needing a window context.
`SyncConnectionState` was a one-variant enum (`SignedOut`), so the
Sync page rendered "Local-only · sign-in coming soon" even after the
bearer token landed on disk and the upload thread completed. The
state machine now mirrors the actual lifecycle.
What lands:
- `SyncConnectionState` gains `SignedIn`, `AwaitingDeviceApproval`,
`SyncReady { last_synced_at_secs }`, `SyncError { message }`.
`SyncObjectState::Synced` joins the per-object enum so individual
rows can advertise "Synced" once a successful upload lands.
- `BrowserCore` stores the current `SyncConnectionState` and exposes
`set_sync_connection_state`. `sync_status` now propagates the live
state into the snapshot the UI reads.
- `ElyShell::probe_initial_sync_state` inspects
`<profile_data>/sync/bearer.token` synchronously at construction
so the first render of the sync page is honest about whether the
user is signed in.
- A `std::sync::mpsc` channel ferries upload outcomes from the
off-thread worker back to the shell; the existing 8 ms tick
drains it and stamps `core.set_sync_connection_state` with the
freshest result. The UI now shows "Signed in · awaiting first
sync", "Synced · last upload Xm ago", "Sync error · …", and the
worker-special "Signed in · waiting for device approval" when the
server returns `device_not_approved`.
Dark mode was persistent in `AppearanceSettings` but never reached any
paint code: every call site in the shell read `colors::INK` etc. as a
`pub const u32`, so toggling `ThemeMode::Dark` mutated state nothing
sampled. Root-cause fix is to invert the contract — the design-system
exports functions that resolve through a thread-local `Mode`, and the
GPUI render impl sets that mode each frame.
What lands:
- `ely_design_system::colors::Mode` + thread-local + `set_mode` /
`mode` accessors. Every ink shade, glass surface, stroke, divider,
hairline, canvas, success / error chip now picks the warm-dark
counterpart when the active mode is `Mode::Dark`.
- `ElyShell::render` resolves `ThemeMode::System` against
`Window::appearance()` and pushes the mode before traversing the
tree, so widgets lower down read the right shade without owning a
`Mode` parameter.
- `render_wallpaper` + `panel_bg` now branch on `colors::mode()` so
the gradient base, panel tint, and overlay highlights flip to
warm-graphite when dark mode is active.
- Mechanical conversion across 687 call sites in 55 files from
`colors::FOO` constants to `colors::foo()` accessors. The
`Theme` / `ELY_THEME` const surface (unused outside the design
system) is removed; the function surface is the new contract.
Wire `SyncEngine::upload_bytes` to a Settings → Sync button:
- `BrowserCore::build_sync_snapshot_bytes` serialises the user's
bookmarks on the UI thread (cheap, synchronous).
- `ElyShell::trigger_cloud_sync_upload` resolves the active
profile data dir, spawns a dedicated `ely-sync-upload` thread,
and lets the engine run the blocking HTTP round-trip there so
the GPUI render loop never stalls on the network — the same
invariant the Servo IPC worker enforces.
- Outcomes go through `tracing` on the `ely::sync` target. Users
drop a Better Auth bearer token into
`<profile_data>/sync/bearer.token` to opt in; without one, the
engine reports `SignedOut` and the click is a no-op.
The Better Auth handshake + device-approval UX still need their
own UI passes; this lands the data-plane plumbing so those pieces
slot in without re-architecting the snapshot path.
Embeds Newsreader (variable opsz/wght, OFL-licensed) into the
ely_app binary via include_bytes!, registers it through GPUI's
text_system at startup (chrome::typography::register_serif_fonts),
and exposes the SERIF_FAMILY constant for downstream surfaces.
Apply the family to every page hero where the design uses
ely-serif: home headline, settings/appearance headline, plugin
marketplace headline, sync headline, plugin detail name, and the
home reading-list cover headline. The OFL license file ships
alongside the .ttf assets to satisfy the font's redistribution
clause.
Sync now opens with the design's two-column structure: a serif headline
and Cloudflare Sync status pill on the left with a local-queue card
showing pending/failed counts and a reset action, and a "What syncs"
card on the right that lists every SyncObjectKind with a state dot,
local count, and a working policy toggle.
The connection label is honest: until Better Auth integration ships, it
reads "Local-only · sign-in coming soon" rather than displaying a fake
sign-in form. All toggles dispatch real shell.set_sync_object_policy
calls so the state changes are persisted through the existing core API.