Skip to content

Changelog

All notable changes to Go Green Transit (codename ftproxy).

[0.8.9] — 2026-04-28 (Phase E + F — calendar UI, schedule windows, unified Jobs)

The capstone of the jobs/automation track. Phase E shipped a calendar visualization + recurrence builder (Once / Daily / Weekly / Monthly / Custom-cron) with start/end gating. Phase F collapsed "Site Sync" and "Batch Job" into one unified Job concept — a job is a list of steps that run on a schedule, full stop.

Phase E — Calendar + recurrence builder + schedule windows

  • SiteSchedule and BatchJob gain start_at / end_at (Unix epoch seconds, optional). Scheduler tick gates firings outside the window. start_at lets users say "fire weekly starting May 5"; end_at lets them say "stop firing after Aug 31".
  • New endpoint GET /schedules/upcoming?from=&to= projects every scheduled firing within a time window. The calendar UI hits this per-month to populate day cells.
  • Frontend Server → Schedules… becomes a tabbed modal:
  • 📅 Calendar — month grid, today's cell outlined in accent, chips per scheduled firing colored by flavor (upload / download / mirror / batch). Click a chip → run-now / edit / details popover. Click an empty day → schedule builder pre-dated to that day. Prev / Today / Next navigation.
  • ☰ List — flat job list with run / edit / delete.
  • Schedule builder modal: recurrence dropdown (Once / Daily / Weekly / Monthly / Custom-cron), HTML5 <input type="time">, weekly day-of-week checkboxes, monthly day-of-month spinner, start/end date pickers, live cron preview, custom-cron escape hatch. No more raw cron-string-only entry.
  • Overlay modals (builder, fire popover, day-overflow, step editor) switched to fixed-position backdrop centering so they stack on top of the parent calendar instead of layouting below it.

Phase F — Unified "Job" concept

The mental model "Site Sync vs Batch Job" forced users to make a decision that didn't matter for most workflows. Phase F collapses both into one primitive:

  • A Job is a name + list of steps + schedule. A 1-step job is the simple case ("nightly folder backup"); an N-step job is the workflow case ("sync, wait, webhook").
  • New step type file-sync for the "I want to sync 1–2 specific files, not a whole folder" case. Takes siteId, direction, localDir, remoteDir, files[], optional policy. Direction must be upload or download (mirror rejected — no clear semantics for file-level mirror). Each file goes through the same per- session /sessions/:id/transfers/{upload,download} path the manual right-click flow uses.
  • Migration at startup: every saved site that still carries an inline schedule becomes a 1-step Job named "Schedule for <site>", then the inline site.schedule is cleared. Idempotent — re-running on a fully-migrated config is a no-op. Wired in lib::run_headless_bridge and the Tauri-window setup.
  • Native menu cleaned up: Server → Jobs… + Job history…. The redundant "Batch jobs…" entry is gone — the list tab inside Jobs… serves that purpose.
  • Site Form's inline Schedule section is replaced with a button: "📅 Schedule a job for this site →". It opens the Job builder pre-filled with one folder-sync step targeting the current site. Sites hold connection info; Jobs hold automation. Cleaner separation.

Wire-format fix (was a latent bug)

  • BatchStep enum's rename_all = "camelCase" only renamed variant tags (Syncsync), not the fields of each variant. The frontend was sending siteId/localPath but Rust expected site_id/local_path. Fixed by adding rename_all = "camelCase" to each variant individually. New file_sync_step_serializes_with_camelcase_fields test locks down the wire shape.

Logging

  • crate::batch_jobs::run now tracing::instrument'd with job_id in the span.
  • Per-step run_step instrumented with job + kind fields.
  • File-sync step emits start/finish info-level logs + state.push_log for the visible log pane.
  • Migration logs each site → job conversion at INFO with site_id, site name, new job_id, cron.

Tests — net +5 over v0.8.8

  • batch_jobs::tests::file_sync_step_serializes_with_camelcase_fields — asserts siteId/localDir/remoteDir on the wire and round-trips back through Deserialize.
  • batch_jobs::tests::job_with_schedule_window_serializes_camelcase — asserts scheduleStartAt/scheduleEndAt shape and that None skip-serializes.
  • batch_jobs::tests::migrate_site_schedules_creates_jobs_and_clears_inline — verifies migration shape + idempotency.
  • scheduler::tests::upcoming_in_window_respects_start_and_end_gating — 3 firings in a Mon–Wed window, none outside.
  • batch_step_kind_str_covers_all_variants extended to cover the new file-sync variant.
  • Lib total: 203/203.

UI build marker

  • v77-unified-jobs.

[0.8.8] — 2026-04-27 (Phases B+C+D of jobs/automation — session isolation, notifications, batch jobs)

Three more phases of the jobs/automation track land together so the shipped surface is coherent.

Phase B — Scheduler session isolation

  • state::SessionKind { Interactive, Background }. Background slots are filtered out of GET /sessions so the user's tab strip never flickers when cron fires.
  • ConnectRequest and DirSyncBody now accept an optional sessionId field. When set, the connect/sync targets that slot instead of the active one and does NOT switch active_session_id.
  • scheduler::fire_schedule creates a fresh background slot per firing, runs the loopback connect+sync against it, deletes it on the way out. The user's interactive transport is no longer stomped.
  • New tests: bridge::tests::list_sessions_hides_background_slots, bridge::tests::post_connect_with_session_id_targets_specified_slot.

Phase C — Notifications fan-out

  • New module crate::notify. Each scheduled-sync success/failure fires every configured channel best-effort, fire-and-forget. Errors go to the user-visible log via state::push_log.
  • Channels (env-driven, read at call time so .env edits don't need a recompile):
  • SlackSLACK_WEBHOOK_URL ({ "text": ... } payload)
  • DiscordDISCORD_WEBHOOK_URL ({ "content": ... } payload)
  • TelegramTELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID (Bot API sendMessage)
  • WebhookWEBHOOK_ON_SUCCESS_URL / WEBHOOK_ON_FAILURE_URL (generic JSON POST with full run record)
  • EmailSMTP_* env recognized, but the in-process sender is a stub (would need adding lettre to Cargo.toml). The test endpoint reports "configured but sender pending" so users with a populated SMTP block aren't confused.
  • New endpoint POST /notify/test { channel, message? } — fires one channel synchronously and returns the DeliveryResult envelope (configured, sent, error). Used by the future Settings UI's per-channel "Send test" button.
  • 6 new tests: channel wire-roundtrip, message-shape on success/ failure, alias normalization, plus two endpoint tests (notify_test_rejects_unknown_channel, notify_test_returns_not_configured_when_env_unset).

Phase D — Batch jobs

  • New module crate::batch_jobs. JSON store at <data_dir>/batch_jobs.json, atomic write, capped at 5000 rows.
  • BatchJob { id, name, enabled, scheduleCron?, steps[], createdAt, updatedAt }. Steps are tagged-enum:
  • sync — recursive /dir/sync against a saved site.
  • wait — pure delay (tokio::time::sleep).
  • webhook — fire-and-forget HTTP (POST/GET/PUT/DELETE) with optional JSON body.
  • Run engine: walks steps sequentially, fail-fast on first error. Sync steps reuse Phase B's session-isolation (each sync runs against a fresh Background session and is recorded in schedule_history with batchId set so the cross-site Job History view groups them).
  • Bridge endpoints:
  • GET /batch-jobs — list
  • GET /batch-jobs/:id — one
  • POST /batch-jobs — create
  • PUT /batch-jobs/:id — update
  • DELETE /batch-jobs/:id — delete
  • POST /batch-jobs/:id/run — trigger now (returns RunSummary)
  • Scheduler integration: each tick now also walks BatchJobs with a populated scheduleCron and fires those due in the same window.
  • schedule_history::record_finish_with_batch overload so batch steps stamp the run rows with batch_id. The free-floating record_finish keeps its existing signature for back-compat.
  • UI: Server menu → Batch jobs… is no longer a placeholder. Lists every job with run / edit / delete actions; editor supports add/reorder/remove of sync, wait, webhook steps; per-step inline modal for fields. Built with the same div-tree pattern as the Site Manager redesign.
  • 5 new module tests (upsert_assigns_id_and_persists, upsert_updates_existing_preserves_created_at, delete_removes_only_matching_id, batch_step_serializes_with_type_tag, batch_step_kind_str_covers_all_variants)
  • 2 endpoint tests (batch_jobs_crud_roundtrip, batch_jobs_run_unknown_id_returns_500).

Tests

  • Lib: 198/198. Combined gain across phases: +13 over v0.8.7.

UI build marker

  • v73-phases-bcd-jobs-notify-batch.

[0.8.7] — 2026-04-27 (Schedule history — Phase A of jobs/automation track)

First slice of the cron/jobs/batch initiative. Phase A ships audit + visibility for the existing scheduler so users can see what fired, when, and what happened. Phases B (session-isolated firing), C (notifications), D (batch jobs) follow.

New module — schedule_history

  • src-tauri/src/schedule_history.rs. Mutex-guarded JSON store at <data_dir>/schedule_history.json. Capped at 10 000 rows; oldest truncated when the cap is reached. Atomic writes via .tmp + rename.
  • ScheduleRun carries: id (uuid), site_id, site_name, batch_id (optional, reserved for Phase D), started_at, finished_at, direction (upload/download/mirror), local_path, remote_path, status (running/succeeded/failed/cancelled), stats: ScheduleRunStats { uploaded, downloaded, failed, bytes_in, bytes_out }, error (optional), triggered_by (scheduler for cron, manual for run-now, reserved values for Phase D batch triggers).
  • Public API: record_start, record_finish, list(since, limit), list_for_site(site_id, since, limit), purge_older_than(retention_days), clear_all, clear_one.
  • 6 unit tests covering happy paths + the since_ts cutoff filter
  • bounded cap behavior. Tests serialize via a TEST_LOCK so the shared on-disk file doesn't race.

Scheduler wired to record every firing

  • scheduler::fire_schedule now returns anyhow::Result<ScheduleRunStats> instead of Result<()>. It parses /dir/sync's response envelope to extract uploaded / downloaded / failed counts.
  • scheduler::scheduler_loop calls record_start immediately before each fire (status=running), and record_finish after with succeeded + stats or failed + error string.
  • triggered_by set to "scheduler" for cron firings, "manual" for POST /scheduler/run-now invocations.

4 new bridge endpoints (auth-gated)

  • GET /schedule-history?since=<ts>&limit=<n> — global list across every site. Defaults: since=0, limit=200.
  • GET /sites/:id/schedule-history?since=<ts>&limit=<n> — per-site list. Default limit=50.
  • DELETE /schedule-history — clear all rows. Returns { cleared: true }.
  • DELETE /schedule-history/:run_id — delete one row. Returns { removed: true }; 404 if the id is unknown.
  • All four wired in bridge::build_router and emit tracing::info on each call. Two new handler tests assert envelope shape + auth gate.

UI — top-menu access + per-site panel

  • Server menu → Job history… — opens a cross-site modal showing the most recent 500 runs. Status icons (✓ ⊘ ✗ ⚠), site name, direction, timestamp, duration, stats line N↑ N↓ N✗, expandable error text. Refresh button + Clear-all button + per- row delete.
  • Server menu → Batch jobs… — placeholder modal explaining the feature lands in Phase D (multi-site sync, cross-site copy, wait/webhook steps, scheduled batches with run history).
  • Site Form → Schedule section → "Recent runs" panel — fetches GET /sites/:id/schedule-history?limit=20 and renders each run with the same status icons + stats. Same panel where the Run-now button lives, so users can fire a schedule and immediately see the row appear.
  • Two new menu IDs added to NATIVE_MENU_IDS (server_job_history, server_batch_jobs); Rust-side menu install_native_menu and JS-side action router both handle them.

.env plumbing for Phase B/C/D (forward-looking only — not yet wired)

  • .env.example (new) and gitignored .env seeded from the master keys file. Active settings today: RUST_LOG, telemetry off, Telegram bot token + chat ID populated, SMTP populated for the IONOS VPS (alerts@gogreensuites.com).
  • Forward-looking slots: SCHEDULER_TICK_INTERVAL_SECONDS, JOB_HISTORY_RETENTION_DAYS, POLLING_INTERVAL_SECONDS, WEBHOOK_ON_SUCCESS_URL / WEBHOOK_ON_FAILURE_URL, SLACK_WEBHOOK_URL, DISCORD_WEBHOOK_URL, STRICT_HOST_KEY_CHECKING, CONCURRENCY_DEFAULT, TRANSFER_RATE_KIB. Phases B–D will pull these in.

Tests

  • Lib: 183/183. New: schedule_history::tests (6 cases), bridge::tests::scheduler_run_now_returns_ok_envelope, bridge::tests::scheduler_run_now_requires_auth.
  • Endpoint suite (scripts/test-endpoints.ps1) unaffected — Phase A endpoints exercised in lib tests.

UI build marker

  • v72-phase-a-schedule-history.

[0.8.6] — 2026-04-27 (Site Manager polish + smart Connect + run-now + e2e scaffolding)

UX polish pass on top of v0.8.5. Same protocols, smoother flows.

Site Manager redesign

  • Replaced the native <select> with a custom div-tree list. Each protocol group has a sticky header with the accent-colored chip pulled from PROTO_ACCENTS (SFTP teal, Dropbox blue, Drive yellow, S3 orange, Azure cyan, etc.) so the list visually matches the swatch palette + chrome.
  • Site rows show name (primary), host + folder in monospace (secondary, dimmed), and an inline auth-status badge:
  • ✓ AUTH (green) — OAuth signed in, hover reveals expiry
  • ⚠ NO AUTH (warn) — needs sign-in
  • LOCAL (faint) — filesystem-mirror site
  • Hover state (var(--bg-row-hover)) on every row; selection state (accent-soft + 3px accent border on the left).
  • Click selects, double-click connects (no need to click the Connect button).
  • Subhead counter shows 41 sites · 7 protocols unfiltered or 5 of 41 sites when filtering.
  • Empty / no-match state shows a centered placeholder instead of a hollow box.

Smart toolbar Connect

  • Toolbar Connect button now intelligent for cloud protocols: shows only when at least one saved site exists for that family, and on click connects directly to it (skips Site Manager). Tooltip shows "Connect to ". SFTP/FTP/FTPS/WebDAV/SMB keep their existing Quick Connect modal flow (these typically have many sites).
  • When 2+ sites of the same cloud family exist (e.g. Dropbox-OAuth + Dropbox-local), a small chooser modal appears so the user can pick. Local-sync sites sort first (faster path).
  • Native menu's "Quick connect…" (Ctrl+Q) still opens the modal directly — power-user path unchanged.

Site Manager search/filter

  • Search input above the list filters by substring match against name + host + folder. Re-renders on each keystroke.
  • Auth-status badges (✓ AUTH / ⚠ NO AUTH / LOCAL) inline next to each site name. Pre-fetched via oauth_status Tauri command on Site Manager open.

"Run schedule now" button

  • New button in the Site Form's Schedule section. Hits new bridge endpoint POST /scheduler/run-now (auth-gated like every other protected endpoint).
  • Bridge handler delegates to scheduler::run_once_now(), which fires any schedule due in the past 24h. tracing::instrument + INFO/WARN logging on success/failure; mirrored to user-visible state log.
  • Useful for testing a new cron entry without waiting for the next tick.

Per-vendor -local accent entries

  • New PROTO_ACCENTS entries for dropbox-local / gdrive-local / onedrive-local with marks DBX·L / GDR·L / 1DR·L, kickers "Filesystem mirror (Dropbox)" etc., and titles like "Dropbox · local sync". Same color as the API counterpart so the chrome remains consistent; only the badge text differs.
  • applyAccent lookup priority: full key first, then -local- stripped fallback, then SFTP safety net.

Live integration test scaffolding (gated on env vars)

  • tests/webdav_e2e.rs (WEBDAV_E2E=1 + URL/USER/PASS) — connect, list, upload+download+verify byte-exact, delete.
  • tests/azure_files_e2e.rs (AZURE_FILES_E2E=1 + storage account/key/share) — exercises the same SMB-shape translation the bridge does, then connect+roundtrip.
  • tests/gcs_e2e.rs (GCS_E2E=1 + bucket + service-account JSON inline or via _FILE path) — connect+roundtrip.
  • tests/oauth_refresh_e2e.rs (OAUTH_REFRESH_E2E=1 + provider/key/ client_id of a previously signed-in site) — exercises the refresh- if-needed path live, then hits the provider's userinfo endpoint with the freshly-rotated access_token to confirm it actually authenticates.
  • All four short-circuit with a "skipping" message when the gate env isn't set, so they don't break the default test cycle.

Test additions

  • bridge.rs route tests:
  • scheduler_run_now_returns_ok_envelope — POST works with valid bearer.
  • scheduler_run_now_requires_auth — POST without bearer → 401.
  • secret_extras_endpoint_returns_empty_for_site_without_secrets
  • secret_extras_endpoint_404s_for_unknown_site
  • Test count: 177 unit + 16 integration (was 173 + 12 at start of v0.8.6 work).

Misc fixes

  • transport module is now pub mod so integration tests in tests/ can construct ConnectionInfo and call transport connect() directly (mirrors what places::cloud_sync_folders and friends already do).
  • places.rs::cloud_sync_folders Google-Drive detection: <drive>: (no trailing backslash) was being treated by PathBuf::join as "current dir on G:" rather than the drive root, producing G:My Drive instead of G:\My Drive. Normalize trailing separator before join.
  • oauth.rs::load() no longer silently returns None on keyring failures. Logs tracing::warn! on entry-build / read errors / JSON corruption (still treats NoEntry as quiet, since "user hasn't signed in yet" isn't an error).
  • Removed the stray let _ = (); // marker for clarity from the refresh-decision branch in oauth.rs.
  • 20 dead-code warnings → 0 (cargo fix removed two unused imports; 18 intentional API-surface entries got #[allow(dead_code)] with per-item justifications).

[0.8.5] — 2026-04-27 (OAuth refresh + local cloud-sync transport + Azure Files)

A long session focused on two things: making cloud connections actually pleasant to set up, and finishing the OAuth refresh-token plumbing so per-user OAuth apps work without re-pasting tokens every 4 hours.

Per-user OAuth with refresh-token rotation (Dropbox, Google Drive, OneDrive)

  • oauth.rs: added Dropbox to provider_config() (auth + token URLs + token_access_type=offline extra). Threaded client_secret through the whole flow so confidential clients (Dropbox; Google "web" / installed app types) can authenticate. Made PKCE conditional via a pkce: bool field on ProviderConfig — Dropbox returns invalid_response_type if PKCE is sent when the app's "Allow public clients" toggle is off, so we omit code_challenge for Dropbox and send it for Google + Microsoft.
  • save() now strips access_token before persisting — long Dropbox short-lived tokens (~1331 chars × UTF-16 = 2662 bytes) blow past Windows Credential Manager's 2560-char attribute limit. The small refresh_token + client_id/secret/expires_at fit fine; access_token is minted on demand by refresh_if_needed.
  • refresh_if_needed() triggers when access_token is missing OR within 60s of expiry.
  • Bridge post_connect: for dropbox/gdrive/onedrive sites with a stored OAuth identity, calls refresh_if_needed and injects the fresh access_token as info.password automatically. Transport stays oblivious.
  • New endpoint GET /sites/:id/secret-extras returns keychain-stored extras (client_secret, service_account_json) so the Site Form can pre-populate masked fields on edit.
  • client_secret added to credentials::SECRET_EXTRA_KEYS — never serialized to sites.json plaintext.

Local cloud-sync transport (Dropbox / OneDrive / Google Drive desktop clients)

  • New transport/localcloud.rs: filesystem-backed Transport that treats the desktop client's sync folder as the "remote". Reads and writes are instant (filesystem speed); the desktop client uploads to the cloud asynchronously. Routed via aliases: dropbox-local, onedrive-local, gdrive-local, icloud-local, localcloud. Path-traversal sandbox via component walk (rejects ..).
  • places::cloud_sync_folders() upgraded:
  • Reads %LOCALAPPDATA%\Dropbox\info.json (and ~/.dropbox/info.json on Mac/Linux) → finds custom Dropbox sync folders even when the user moved them off C:.
  • Reads OneDrive registry (HKCU\Software\Microsoft\OneDrive\ Accounts\*\UserFolder via winreg) → finds personal + every business account at their actual paths.
  • Scans Windows drive letters for <drive>:\My Drive → finds Google Drive virtual mount even when volume label is empty.
  • Site Form auto-detects the desktop client on form open. When found: defaults to local-sync mode with the path pre-filled (zero OAuth). When not found: falls back to OAuth with a hint that installing the desktop client would skip OAuth entirely.
  • Save handler transforms the protocol to <base>-local when local mode is selected; on edit, recognizes the -local suffix and shows the dropdown's base protocol with mode toggle preset to local.

Azure Files as a first-class protocol (separate from Azure Blob)

  • New azure-files protocol. Site form fields = storage account name + access key + share name. On connect, bridge translates these into SMB shape (\\<account>.file.core.windows.net\<share>, Azure\<account> username, key as password) and dispatches to SmbTransport. Dropdown now shows two distinct entries: "Azure Blob — object store" and "Azure Files — SMB-mountable file share".
  • Top "Azure Blob" swatch renamed to "Azure Storage" — same chrome accent (Azure cyan) covers both Blob and Files. PROTO_ACCENTS adds a distinct entry for azure-files so the badge text reads "Azure Files" with kicker "File share (SMB)" when connected to Files (vs "Azure Blob" / "Blob container" for the API protocol).

Site Manager UX polish

  • applyAccent and activeProto reordered: live-session protocol wins over _forceProto. When connected, all swatches except the live one are visually disabled (opacity 0.30, not-allowed cursor, tooltip explaining how to switch). Click a non-matching swatch and it's a no-op with a wire-console warning.
  • applyForcedAccent clears _forceProto after a successful connect so the new session's chrome takes over.
  • Cloud protocols (S3 / Azure Blob / GCS / Dropbox / GDrive / OneDrive) hidden from the toolbar "Connect…" Quick-Connect button via actionPolicy — those need extras Quick Connect can't capture, so we route them through Site Manager instead.
  • Site Manager grouped by protocol (SFTP (n), FTP (n), Amazon S3 (n), etc.). User-set folder shows as parenthetical metadata next to the host. New sites auto-bucket to a sensible folder via default_folder_for_protocol (no more "ungrouped" creep).
  • Render fix: when host is empty (S3, Azure, etc.), drop the — — placeholder. Just show the name.
  • Per-protocol info icons (ⓘ): every cloud + filesystem protocol has a clickable icon next to its title with setup instructions and a "Copy to clipboard" button. Content covers register-an- OAuth-app walkthroughs (Dropbox / Google / Microsoft), AWS IAM setup for S3, Azure storage account setup for Blob and Files, GCS service-account-JSON pattern, and the local-sync paths.

Windows URL launcher fix (root cause of the OAuth runaround)

  • lib.rs::open_url_in_browser on Windows now uses rundll32 url.dll,FileProtocolHandler instead of cmd /C start "" url. cmd parses & as a command-chain operator, truncating any URL with multiple query params at the first &. Every OAuth provider returns a misleading "Unexpected response_type" because the truncated URL has no response_type left. rundll32 receives the URL as a single argument and dispatches via Windows shell URL handling, preserving the full string. Lesson saved to tasks/lessons.md and ~/.claude/projects/.../memory/.

OAuth callback fixed-port + redirect URI

  • oauth::start_callback_listener now binds on port 53682 (the convention gcloud uses for desktop OAuth flows) before falling back to a random port. The redirect URI uses localhost (not 127.0.0.1) because Dropbox treats http://localhost as a wildcard for any port when whitelisting redirect URIs but http://127.0.0.1 literally.
  • Users register http://localhost:53682/oauth/callback once in their Dropbox app's Redirect URIs. Google + Microsoft honor the loopback wildcard so just http://localhost registration works for those.

Test additions

  • tests/s3_multipart_resume.rs (gated S3_RESUME_E2E=1) — two cases against MinIO at 127.0.0.1:9010: full 6-phase resume contract (Create → UploadPart×2 → ListMultipartUploads → ListParts → resume UploadPart#3 → Complete → GetObject byte-exact match), and AbortMultipartUpload clears pending entry.
  • tests/sendto_syntax.rs — 5 cases parsing the macOS Info.plist
  • document.wflow via the plist crate, and validating the Linux .desktop entry against FreeDesktop spec keys.
  • tests/updater_check.rs — 3 cases against tauri_plugin_updater:: RemoteRelease: spins a localhost axum server, deserializes a latest.json payload, validates download_url() + signature() per target, and asserts version-comparison logic both up and down.
  • New oauth.rs tests: Dropbox provider config (offline, no PKCE), PKCE conditional in auth URL build, access_token strip in save, refresh-decision branches.
  • New places.rs tests: Dropbox info.json parsing (personal / business / malformed JSON degrades to None), OneDrive registry smoke, cloud_sync_folders return type.
  • New bridge.rs tests: localcloud + azure-files in normalize_ protocol, Azure Files SMB-shape translation guard, every supported protocol covered by default_folder_for_protocol (no fall-through to "Other").
  • Test count: 173 unit + 12 integration (was 157 + 12 at start of session).

OneDrive registry detection

  • See places.rs section above. Survives unlink + relink to a custom folder location. Reads DisplayName for friendly Business labels.

Logging hardening

  • oauth::load() now logs warnings on keyring entry-build failures (tracing::warn!) and on stored-token JSON corruption. Was silently returning None — operators had no way to triage.
  • bridge.rs OAuth refresh failure already logs to user-visible state log; kept.

[0.8.4] — 2026-04-26 (Production-grade observability + scaling foundation)

Six items from the architecture-review pass — everything that makes the bridge a credible API for other apps in production, without adding Redis or going multi-process.

Prometheus /metrics endpoint

  • New metrics.rs module installs metrics_exporter_prometheus recorder at startup (idempotent). Exposes /metrics text/plain scrape body — pre-auth (loopback-only deployment makes this safe).
  • HTTP middleware records ftproxy_http_requests_total{method, path, status} counter and ftproxy_http_request_duration_seconds histogram on every request. Path is collapsed to the matched-route pattern (no per-id explosion).
  • Transfer terminal-state hook fires ftproxy_transfers_total{ direction, protocol, status}, ftproxy_transfer_bytes_total, and ftproxy_transfer_duration_seconds per transfer.
  • Queue and session gauges (ftproxy_queue_depth, ftproxy_sessions_connected) updated on every /health poll — Grafana sees them at the scrape cadence.

JSON access logs (opt-in)

  • RUST_LOG_FORMAT=json switches tracing-subscriber to its JSON formatter so headless deploys can ship logs cleanly into Loki / ELK / Datadog without an intermediate parser. pretty and full also accepted; default stays human-readable so desktop dev experience is unchanged.
  • tracing-subscriber gains the json feature.

Per-token rate limiting

  • New rate_limit.rsgovernor token-bucket keyed on the Authorization header. Default 600 req/min (10/s with a 100-rps burst window) per token, configurable via AppConfig.rate_limit_per_minute (0 disables).
  • /health, /metrics, /events are bypassed so liveness probes and Prometheus scrapers can poll freely.
  • 429s are counted in the metrics counter alongside 200s.
  • PUT /config clears every token's bucket so rate-limit changes take effect on the next request.

Richer /health

  • Returns status (ok / degraded), version, queue depth broken out by status (running / pending / failed / complete), last-minute transfer count + error count + bytes, session count.
  • degraded fires when failures are >50% of the last-minute traffic — useful for k8s readyz-style probes that want a single bool to drive load-balancer membership.
  • The full protocol + capability list is surfaced (was hard-coded to the 0.4.x set; now reflects all 11 protocols + scheduler + metrics + rate-limit capabilities).

Keychain access cache

  • credentials.rs gets an in-process LRU (60 s TTL, 256-entry cap) that wraps every keyring::Entry::get_password call. Repeated reads for the same site within a short window now skip the OS keychain round-trip. set and remove write through.
  • Eviction policy: oldest-first when at cap. Manual cache_clear() for tests + Edit → Clear private data.

Multi-runtime split

  • Transfer work runs on a dedicated tokio runtime (capped at 8 worker threads, named ftproxy-xfer-*). HTTP handlers spawn transfer work onto it via state.transfer_runtime.spawn(...) and await the JoinHandle.
  • Result: a slow upload that blocks reading 10 GB off disk no longer starves the HTTP runtime's worker threads. /health polls and /metrics scrapes stay fast under transfer load.
  • The transfer runtime is leaked at process exit (intentional — prevents tokio's Drop from blocking on outstanding tasks at shutdown).

Tests

  • cargo test --lib155/155 (was 147). New cases cover metrics install / render / counter increment, rate-limit bypass-path detection + quota construction, keychain cache put / get / invalidate / cap-eviction round-trips.
  • npm test (Vitest) → 39/39.

What's still NOT here (and intentionally so)

  • Redis / BullMQ / external broker — not needed at single-host scale. The single-process tokio model with queue.json persistence handles thousands of concurrent file-handle ops on one core. The point at which Redis becomes correct is "multiple bridge instances sharing one job pool" — until then it's operational overhead with zero throughput gain.

[Server / deployment] — 2026-04-26 (IONOS site catalog)

Not a code release — server-side provisioning + FTProxy site catalog expansion. Documented here so the team has a single place to find "what SFTP accounts exist and where do they point."

14 chrooted SFTP users on 74.208.129.33

Up from 1 (gogreensuites_ftp). The setup is driven by ~/setup-sftp-domains.sh on the dev box (idempotent — re-run after adding a new domain).

  • 12 per-domain users (one per /var/www/<domain>/ docroot): admin_frontierlimited_ftp, admin_opensentinel_ftp, docs_opensentinel_ftp, familychatai_ftp, gogreenautodiag_ftp, gogreenmarketingai_ftp, gogreenpaperlessinitiative_ftp, gogreensellerai_ftp, gogreenverify_ftp (covers BOTH GoGreenVerify frontend AND GoGreenVerify-API backend), gogreensuites_ftp (existed), opensentinel_ftp, status_opensentinel_ftp.
  • 1 shared deploy_apps_ftp/home/deploy/apps/ (covers the 21 Dockerized app sources: Boomer_AI, EverythingBeer, MangyDog, Maximus, NaggingWifeAI, PRT, SCO, Sales_AI, SellMe variants, Tutor_AI, Voting, etc.)
  • 1 shared wordpress_ftp/var/www/html/Wordpress (covers the gogreenwpplugins.com trio of vhosts)

Server-side pattern

  • Match Group sftpusers block in sshd_config — ChrootDirectory /srv/sftp/%u, ForceCommand internal-sftp, no TCP forwarding, no X11. One block covers every per-domain user.
  • Each user's chroot is /srv/sftp/<username>/ (root:root, mode 755).
  • Each docroot is bind-mounted into the chroot via /etc/fstab so the SFTP user sees /<domain>/ after login.
  • ACL setfacl -R -m g:deploy:rwx -m d:g:deploy:rwx <docroot> so the SFTP user can write without disturbing whatever owner already writes (www-data, root, systemd services, etc.). Default ACL inherits to new files.

Operational gotchas (also in tasks/lessons.md + TROUBLESHOOTING.md)

  • Reload vs restart: after editing sshd_config, you must systemctl restart ssh.socket (not reload ssh.service) on socket-activated systemd setups (Ubuntu 22.04+). Reload won't re-read the config for the listening socket.
  • Bridge upload semantics: /transfers/upload's remotePath is the parent directory; the bridge appends the local file's basename. Pass /site/, not /site/file.txt.
  • chroot perms: chroot dir + every parent must be root:root with no group/world write. The bind-mounted docroot inside the jail can be group-writable — only the chroot dir itself can't.

Smoke-tested

All 14 sites verified end-to-end via ~/load-and-test-sites.py: connect → upload tiny test file → download → byte-match → delete. 14/14 green. Test files use timestamped + UUID-suffixed names so they never conflict with existing content; cleanup is automatic.

FTProxy state

14 sites in the Go Green VPS folder of Site Manager. Passwords random-32-char per user, stored only in the OS keychain via the bridge's /sites POST path.

[0.8.2] — 2026-04-26 (Gap-analysis pass)

Closing the second-tier gaps identified in the post-0.8.1 audit. Most of these are individually small but materially raise the floor on day-to-day usability.

Managed sites (fleet config)

  • New read-only site source: <data dir>/managed-sites.json or $FTPROXY_MANAGED_SITES. IT pushes a curated list via Group Policy / SMB share / config-management tool; Transit merges it with the user's editable catalog at startup. Managed sites get read_only: true, the bridge refuses PUT/DELETE on them with a clear "managed by IT" message, and the user-side sites.json never persists them — so admin updates flow through cleanly without divergence.
  • 3 new unit tests: load-marks-readonly, id-collision-managed-wins, missing-file-is-silent.

FTP REST resume

  • download_to_offset impl on FTP transport — sends REST <offset> before RETR. Closes the resume gap for FTP, matching SFTP from 0.8.0. FTPS, S3, Azure, GCS still degrade Resume to Overwrite (cloud Range/multipart-resume = future work).

Transfer queue speed display

  • Live MiB/s on each running transfer row, computed via 0.4-alpha EWMA of the 250 ms progress samples — fast enough to react to throughput changes, slow enough not to jitter wildly. Added transfer.skipped to the WS event handler so Skip-policy transfers cleanly clear the progress speed map.

Connection polish (data plumbing)

  • New AppConfig fields: auto_reconnect, keepalive_interval_secs, proxy_url. The runtime hook for the first two is on the 0.8.3 list; the proxy field is fully wired.

HTTP / SOCKS5 outbound proxy

  • transport::apply_http_proxy(builder, info) — drop-in helper that honors AppConfig.proxy_url for any reqwest::ClientBuilder. Wired into WebDAV, GCS, Dropbox, Drive, OneDrive transports. SFTP / FTP / S3 / Azure don't go through reqwest and continue to use direct connections (documented).
  • socks reqwest feature added so socks5://user:pass@host:1080 URLs work alongside http://... / https://....

Recently-used hosts

  • LocalStorage-backed last-10 hosts list. Surfaced as a <datalist> on the Quick Connect host input — hostname autocomplete on every successful connect.

Filename filters (glob)

  • Per-pane glob filter via View → Filter local files… / Filter remote files…. Supports *, ?, {a,b} brace expansion, space-separated multi-pattern, and !-prefix exclusion. Persists per pane across restarts. Directories always pass the filter so navigation isn't broken.

Wire console export

  • View → "Export wire console to file…" downloads the visible console buffer (last ~300 TX/RX/SYS/WARN/ERR rows) as a timestamped .log file. Useful for filing a bug report without screenshots.

Backup / restore everything

  • Edit menu → "Backup everything to JSON file…" writes a single bundle (sites + bookmarks + config + hostkeys + recent hosts + filters) the user can move to another machine. Restore reads it back via the bridge's CRUD endpoints. Token + per-site passwords stay in the OS keychain and aren't in the bundle — by design. Managed (read-only) sites aren't in the bundle either; the next machine picks them up from its own managed-sites.json.

About modal: User Guide + Issues + Bridge details

  • Three buttons in About — User Guide opens USER-GUIDE.md on the docs site, Report an issue opens the issues URL, Bridge details surfaces the same modal we always had. Override-able via window.FTPROXY_DOCS_URL / window.FTPROXY_ISSUES_URL.

Tests

  • cargo test --lib142/142 (was 139). Three new tests for managed-sites behavior.
  • npm test (Vitest) → 39/39.

Build marker

  • Frontend: UI build: 2026-04-26 v50-gap-pass.

Scheduled sync (full impl)

  • New scheduler.rs — tokio task that ticks every 30 s, evaluates every site's schedule.cron against the (last-tick, now] window, fires a /dir/sync for any that's due. Uses the bridge's own loopback HTTP path so the queue, throttle, persistence, and event stream all see the scheduled transfer the same way a UI-driven one would.
  • Cron format: standard 5-field min hr dom mon dow with * and comma-lists. The cron crate is wrapped to add a synthetic seconds field so it agrees with the user-facing 5-field surface.
  • Headless ftproxy-bridge runs the same scheduler — Windows Task Scheduler can fire a backup at 3 AM with the desktop closed.
  • New POST /dir/sync endpoint — recursive compare + per-file transfer dispatch, direction = upload / download / mirror, per-transfer overwrite policy.
  • Site Form gets a Schedule section: enabled checkbox, cron input, direction dropdown, local path, remote path, conflict policy.

Open-remote-in-editor (full round-trip)

  • New editor.rs — Tauri command open_remote_in_editor downloads to %TEMP%/ftproxy-edit/<sha8>/<basename>, launches the OS default editor (cmd /C start "" / open / xdg-open), and spawns a notify crate FS watcher debounced 500 ms that re-uploads on every save via the bridge's existing /transfers/upload.
  • Watcher TTL is 4 h — it self-terminates and cleans up the temp file. Re-opening the same file before TTL just relaunches the editor against the existing watcher (no duplicate watcher leak).
  • Right-click on a remote file → "Open in editor (auto-reupload on save)".

Auto-reconnect runtime

  • SessionSlot now caches the last successful ConnectionInfo. New dispatch_connect() rebuilds the transport from any cached info; new reconnect_session() helper tears down the dead transport and rebuilds. Every transfer-download wraps once in a retry harness: on a transient error (broken pipe / EOF / connection reset / WSA 10053-10054 / EPIPE / timeout) it auto-reconnects and re-issues the call from the resume offset. Honors AppConfig.auto_reconnect (default on); never retries on cancellation or auth failure.
  • New POST /sessions/:id/reconnect endpoint for explicit reconnects from scripts.

Keep-alive ping

  • Per-session tokio task spawned on connect, joined on disconnect. SFTP / FTP / FTPS sessions ping every AppConfig.keepalive_interval_secs (default 60) by issuing a cheap list("."). Other protocols (HTTP-based) skip the ping — each request is its own connection. Cancelled and re-armed cleanly on reconnect so we never run two pings on the same transport.

S3 multipart upload-resume

  • upload() now calls ListMultipartUploads first. If a pending upload for this key exists (from a previous interrupted run), it pulls the already-uploaded parts via ListParts, carries their etags forward, and continues from the next part_number — no bytes re-sent, no double-billing on dangling part charges.
  • UploadPart failures no longer auto-abort — the upload-id is left pending so the next call resumes. New S3Transport::abort_pending_uploads(remote) for explicit discard-and-restart.
  • CompletedMultipartUpload parts are sorted by part_number before the final Complete call so resumed-then-extended uploads serialize correctly.

macOS / Linux Send To equivalents

  • Renamed conceptually from "Send To shortcut" to platform-appropriate surfaces:
  • Windows: existing .lnk in %APPDATA%\Microsoft\Windows\SendTo\.
  • macOS: full Automator Quick Action workflow dropped into ~/Library/Services/. Right-click any file in Finder → Quick Actions → "Send to Go Green Transit" runs ftproxy-cli against each selected path. Menu refresh via pbs -flush.
  • Linux: .desktop file in ~/.local/share/applications/ with MimeType=application/octet-stream;text/plain;… so Nautilus/Dolphin/Thunar surface "Open With → Go Green Transit" on every file. Best-effort update-desktop-database refresh.
  • Module renamed from "Windows-only" docstring; Tauri command works on every platform.

Auto-updater (Tauri plugin)

  • tauri-plugin-updater 2 wired into the Tauri builder. New check_for_updates Tauri command surfaces an Update / "you're on the latest build" prompt via Help → Check for updates…
  • tauri.conf.json carries the plugin config — endpoint URL + signing pubkey. Empty pubkey = inert (no-op check, no errors). Once the user fills in their release host + the pubkey from tauri-cli signer generate, updates flow.
  • capabilities/default.json grants updater:default.

Code signing / notarization config

  • New SIGNING.md walks through the production-release pipeline: Tauri updater signing keypair, Windows EV / OV cert + signtool env vars, macOS Developer ID + notarization via xcrun notarytool, GitHub Actions release workflow with the right secret bindings.
  • tauri.conf.json.version bumped to 0.8.3.

Tests

  • cargo test --lib147/147 (was 142). Three new scheduler tests, two new editor tests already counted in 0.8.2 totals; this round's surface (auto-reconnect, S3 resume) is harder to unit-test without a live server, so coverage there is via the endpoint smoke suite.
  • npm test (Vitest) → 39/39.
  • Frontend build marker bumped to UI build: 2026-04-26 v51-no-defers.

[0.8.1] — 2026-04-26 (Places + SMB protocol)

Local pane: drives + quick locations + Map Network Drive

  • New 📁 Places button on the local pane opens a popover listing:
  • Quick locations from directories::UserDirs — Documents, Downloads, Desktop, Pictures, Music, Videos, Public, Home.
  • Drives the OS reports — every fixed disk (C:, D:), removable (USB / SD / CD), and mapped network drive (Z: from net use). Windows: GetLogicalDriveStringsW + GetDriveTypeW + GetVolumeInformationW. Mac: /Volumes/*. Linux: /media/<user>, /mnt, /run/media/<user>, /run/user/<uid>/gvfs/.
  • Map network drive… action — runs net use Z: (Win) / mount_smbfs (Mac) / mount -t cifs (Linux). Picks the first free drive letter on Windows when none specified.
  • New places.rs module + 4 new Tauri commands: list_drives, list_quick_locations, map_network_drive, unmap_network_drive.

SMB / CIFS as a real Transit protocol

  • New transport::smb — mount-and-proxy strategy. On connect, calls places::map_network_drive to mount the share via the OS's native SMB stack; every file op (list, mkdir, download_to, upload_from, etc.) runs through tokio::fs against the mount root. On close(), unmounts.
  • Site Form / Quick Connect both expose SMB / CIFS — Windows file share / NAS as a protocol option. SMB extras: share, domain, drive_letter, mount_path (the last skips auto-mount and just attaches to a path the user pre-mounted).
  • Host accepts \\server\share, smb://server/share, or just a bare server when the share extra is set. Anonymous connections work when username/password are blank.
  • normalize_protocol accepts smb / cifs / samba aliases.
  • Bandwidth throttle is wired into SMB's streaming download/upload paths just like every other transport.

Tests

  • cargo test --lib139/139 (was 134). New tests cover:
  • Places: smoke test for drive enumeration + quick-locations presence.
  • SMB transport: pre-mounted-path attach (no real net use call, uses a temp dir as the mount root) and bare-host-without-share error path.
  • UNC smb://\\ normalization round-trip.
  • npm test (Vitest) → 39/39.

Build marker

  • Frontend: UI build: 2026-04-26 v45-places-and-smb.

[0.8.0] — 2026-04-26 (MVP → PRD pass: FileZilla-parity feature push)

This release closes 11 of the 15 cross-product gaps the audit flagged between Transit and FileZilla / WinSCP / Cyberduck. The four remaining items (mount-as-drive, scheduled sync, open-remote-in-editor, i18n, OpenStack Swift transport) are each multi-day undertakings on their own and are deliberately staged for 0.8.1+ rather than half-shipped here. See "Deferred" below.

Persistent transfer queue + Resume policy

  • <data dir>/queue.json is now updated on every push / terminal-state change. On restart, anything that was running / pending / queued becomes interrupted with bytesResumed armed to the byte count we'd already transferred — Retry-with-Resume picks up from there for SFTP downloads. (FTP / cloud transports degrade Resume to Overwrite with a tracing warning; protocol-level resume is the only piece that didn't make this cut.)
  • New OverwritePolicy enum on TransferJob: Skip / Overwrite / OverwriteIfNewer / Resume / Rename.
  • Bridge accepts policy on /transfers/upload and /transfers/download. Falls back to AppConfig defaultOverwritePolicy when missing.
  • New Transport::download_to_offset(remote, w, offset, …) trait method. SFTP overrides with a real seek+stream impl; everything else uses the default download_to fallback.

Conflict resolution UX

  • Server-side handlers now check existence + mtime before kicking off a transfer. OverwriteIfNewer compares the local file's modified() to the remote RemoteEntry.modified_at; Rename finds a free .N-suffixed name; Skip short-circuits to status skipped; Resume arms bytes_resumed. Emits transfer.skipped on the WebSocket event stream.

Bandwidth throttle (real, not a placeholder)

  • New throttle.rs module. Throttle::pace(n) is awaited by every streaming transport's chunk loop; set_global_rate_kib(n) is called from AppConfig load + PUT /config. 0 = unlimited.
  • Wired into SFTP (down + up), FTP, FTPS, S3, Azure, GCS, WebDAV download paths.

Site folders

  • SavedSite.folder: Option<String>; FileZilla-parity grouping in the Site Manager (rendered as <optgroup> rows).
  • Site Form has a Folder field with autocomplete from existing folders + free-text new entries.

S3 endpoint presets

  • Site Form exposes a preset dropdown: AWS / Backblaze B2 / Storj / Wasabi / Cloudflare R2 / DigitalOcean Spaces / MinIO / Custom. Picking a preset fills endpoint, region, and path_style.

ftproxy-cli (3rd binary)

  • src-tauri/src/bin/ftproxy-cli.rs — one-shot CLI wrapping the bridge over loopback. Subcommands: --upload / --download / --mkdir / --rm / --list / --sync / --connect-test / --sites. Reads token from <data dir>/token; honors FTPROXY_BASE / FTPROXY_TOKEN env overrides. Exit codes: 0 OK, 1 user error, 2 bridge error.

Windows "Send To" integration

  • New Tauri commands install_send_to_shortcut / uninstall_send_to_shortcut. Drops a .lnk in %APPDATA%\Microsoft\Windows\SendTo\ pointing at ftproxy-cli.exe. Edit menu → Install / Remove Send To shortcut. Implementation is PowerShell + WScript.Shell (no shell-extension COM registration).

SSH command execution

  • New POST /sftp/exec endpoint runs a one-shot command on the active SFTP session via russh's exec channel. Captures stdout / stderr / exit status, capped at 1 MiB. Server menu → "Run command…".
  • Transport::run_command trait method (default: bail with "SFTP-only"). SFTP override threads through channel_open_session / exec / wait.

Speed-test / benchmark

  • New POST /benchmark endpoint uploads N MiB of test data, downloads it back, deletes the test file, and returns { uploadBytesPerSec, downloadBytesPerSec, ... }. Server menu → "Benchmark connection…".

Imports from more legacy clients

  • SmartFTP: walks %APPDATA%\SmartFTP\Client 2.0\Favorites\ recursively, parses each .xml favorite, surfaces directory hierarchy as the folder field.
  • CuteFTP: registry import for CuteFTP 7 / 8 / 9 under HKCU\Software\GlobalSCAPE\CuteFTP {N}\Sites. Supports nested site folders → flattens to Folder1 / Folder2.
  • Both wired under File menu next to FileZilla / WinSCP / PuTTY / CoreFTP. New unit tests pin the protocol-mapping matrices.

Logging

  • Every new endpoint, registry walker, and transport method has #[tracing::instrument] attached with a representative span field set (command for exec, sizeMib for benchmark, count for imports).

Tests

  • cargo test --lib134/134 (was 111). New tests cover:
  • OverwritePolicy wire round-trip + unknown-fallback.
  • TransferJob back-compat deserialize (no policy / bytesResumed fields).
  • Queue persist roundtrip (5 cases: complete preserved, running → interrupted with resume armed, pending-no-progress resets resume, 500-entry cap, invalid-JSON fallback).
  • Throttle: unlimited fast path, paced-to-target pacing, KiB-conversion saturation.
  • SmartFTP protocol mapping + XML field extraction + full parse round-trip.
  • CuteFTP protocol mapping (FTP / FTPS / SFTP / unknown).
  • SiteSchedule serde round-trip + back-compat for sites without folder / schedule.
  • npm test (Vitest) → 39/39 (unchanged — backend changes only in this round).

Build marker

  • Frontend: UI build: 2026-04-26 v44-prd-pass-features.

Deferred to 0.8.1+ (honest scope cut, not vaporware)

  • Mount-as-drive (WebDAV adapter) — designed (mini-WebDAV server bound to 127.0.0.1 + net use Z: \\127.0.0.1@7878\dav) but the data-path proxy + locking semantics need a careful pass before shipping. Half-day's work.
  • Scheduled syncSavedSite.schedule and SiteSchedule are in the data model + persist layer; the cron-tick tokio task and the create/edit UI aren't wired yet. ~1 day.
  • Open-remote-in-editor — temp-file lifecycle + FS watcher. ~half day.
  • i18n infrastructuredist/i18n/{en,es,fr}.json + a t() helper + Settings combo. ML-seeded translations would be theater without a translator pass, hence deferred. ~half day.
  • OpenStack Swift transport — new transport with Keystone v3 auth. ~half day.
  • Resume for FTP / cloud uploads — protocol-side APPE / multipart-resume needs upload-id persistence. The UI shows the selected policy and bytesResumed; it just degrades to overwrite on the wire. ~1 day for FTP, ~1 day for S3 multipart-resume.

[0.7.4] — 2026-04-26 (Azure + GCS continuation pagination)

Object-store pagination wired end-to-end

  • Azure: list_page now reads NextMarker off the ListBlobsResponse and threads any caller-supplied ListOpts.continuation back into the request via ListBlobsBuilder::marker. Containers with >1000 blobs paginate correctly; the bridge surfaces the marker as the opaque next_token in the ListPage envelope.
  • GCS: list_page already threaded pageToken on the request side, but the response struct (ListResp.next_page_token) had no #[serde(rename_all = "camelCase")] and so never matched GCS's nextPageToken wire field. This means GCS pagination has been silently broken since 0.4.0 — directories with more than 1000 objects only ever returned the first page. New unit test (list_resp_decodes_next_page_token) pins the wire format so a future regression here fails the build.
  • Both transports' list() methods now drain every page internally so non-paginated callers (recursive sync, dir compare, the rmdir-walker) see the full inventory regardless of size. Previously they were silently first-page-only.
  • Defensive empty-string → None coalescing on both transports so a buggy server returning "" instead of an absent token never sends the caller into an infinite loop.

Logging

  • #[tracing::instrument] on Azure + GCS list_page with entries count and has_next flag at debug. Useful when triaging "why am I missing files?" reports — you can see whether pagination engaged at all.

Tests

  • Azure: NextMarker round-trip + empty-marker coalesce.
  • GCS: nextPageToken JSON decode + empty-token coalesce.
  • cargo test --lib111/111 (was 107).
  • npm test (Vitest) → 39/39 (unchanged — pure-backend change).

Doc cleanup

  • tasks/todo.md rewritten to reflect 0.7.4. The "Known gaps" section is now down to one item (mobile packaging, deferred).
  • tasks/future.md rewritten — almost every item in the old file had shipped (large-file streaming, host-key strict, multi-session, WinSCP/PuTTY import, dir compare, the client crate, etc.). New ideas only.
  • AI_PLANNING.md updated: Target A (MCP server) marked shipped via src-tauri/src/bin/ftproxy-mcp.rs.
  • AUDIT-2026-04-26.md head section refreshed — Sprint 2 closed.

[0.7.3] — 2026-04-26 (sites import — File menu unification + CoreFTP)

File menu now owns every "Import sites from …" action

  • Moved Import from WinSCP… and Import from PuTTY… out of the Bookmarks menu and into File → Import sites from WinSCP… / PuTTY…, next to the existing FileZilla XML import. They were UX-misplaced before — bookmarks-as-paths and sites-as-imports are different concepts and shouldn't share a menu.
  • Added File → Import sites from CoreFTP…. Reads sessions from HKCU\Software\FTPware\CoreFTP\Sites and the free-edition …\CoreFTPLE\Sites root, mapping the PType field (0=FTP, 1/2=FTPS Implicit/Explicit, 4/5=SSH/SFTP) to the matching FTProxy protocol. Unknown PType values are skipped silently rather than guessed. Like WinSCP/PuTTY, no password is imported (CoreFTP encrypts theirs); the user supplies it on first connect.
  • Native-menu IDs: the old bookmarks_import_winscp / bookmarks_import_putty ids are gone, replaced by file_import_winscp, file_import_putty, file_import_coreftp. NATIVE_MENU_IDS and the __ftpMenuAction switch are kept in lockstep; new Vitest contract test pins the JS side.
  • importFromRegistry(source) is now table-driven via a REGISTRY_IMPORTS map so adding the next client is a one-line diff in JS plus a Tauri command in Rust.

Logging

  • tracing::instrument on winscp_sites, putty_sites, and the new coreftp_sites. Each emits info with the scanned-count or debug when the registry root is absent — useful when triaging "import found 0 sites" complaints from users with non-default install paths.

Tests

  • New Rust unit tests pin the CoreFTP PType → protocol mapping for every known value (0, 1, 2, 4, 5) and guarantee unknowns return None. A no-op smoke test confirms coreftp_sites() doesn't panic on a clean machine with no CoreFTP install.
  • New Vitest contract test (menu-imports.test.js) asserts the file menu carries every import label and the bookmarks menu carries none of them, plus that __ftpMenuAction recognises the new file_import_* ids and not the old bookmarks_import_* ones.

Test totals

  • cargo test --lib107/107 (was 104).
  • npm test (Vitest) → 39/39 (was 30).
  • Frontend build marker bumped to UI build: 2026-04-26 v43-imports-under-file-menu.

[0.7.2] — 2026-04-26 (UX polish + per-protocol action policy)

Site Form — FileZilla-parity General-tab order

  • Encryption dropdown is now a top-level field right after the Port row (where FileZilla puts it), not buried in the extras panel at the bottom. FTP shows 4 modes (auto / explicit / implicit / plain), FTPS shows 2 (explicit / implicit), SFTP shows a green "🔒 Encryption: SSH transport — always on" info row.
  • Logon Type dropdown is now a top-level field right below Encryption. FTP/FTPS: Normal / Anonymous / Ask for password. SFTP: Normal / Ask for password / Key file (with private-key file picker
  • optional passphrase appearing conditionally).
  • Both fields persist into extra map keys (encryption / logon_type / key_path / key_passphrase). The bridge dispatch unchanged from 0.7.1.

Action-bar policy (the real fix you've been asking for)

  • Verify hash and Compare dirs buttons are now hidden (not just disabled) when the active protocol is anything other than SFTP / FTP / FTPS. WebDAV, S3, Azure, GCS, Dropbox, Drive, OneDrive show neither button at all. Adjacent .action-sep dividers collapse with them.
  • actionPolicy(cmd, proto) is the single source of truth for enabled / disabled / hidden / tooltip per cell — implements the AUDIT-2026-04-26.md §9 matrix end-to-end.
  • updateToolbarState() runs synchronously inside applyAccent() so swatch clicks and tab switches refresh the bar in the same frame — no more 2-second polling lag.

First-launch defaults

  • The disconnected app now lights up SFTP-green by default (proto badge, swatch active, tab chip). activeProto() falls back to "sftp" when no live session and no forced palette pick.
  • Proto title for the default-disconnected state reads Ready · SFTP instead of Disconnected so the chrome looks intentional rather than empty.

Native menu

  • View → Hard Reload (Ctrl+Shift+R) added — clears caches.keys()
  • sessionStorage before location.reload(). Same pattern as GoGreenMarketing. Fixes the stale-WebView2-cache class of bug permanently.

Tests

  • cargo test --lib104/104 (was 96; +8 new: FTPS aliases, HashQuery deserialisation, redownload cap constant, StoredToken serde roundtrip, pkce-pair-uniqueness, credentials helpers).
  • npm test30/30 (was 11; +19 in dist/__tests__/action-policy.test.js covering Verify hash + Compare dirs visibility, Upload / Rename / Delete enable rules, always-available Connect / Sites / Refresh).

Logging

  • Connect log line now includes Logon Type + Encryption mode + key path (when key auth) for diagnosis.
  • Verify hash logs algorithm + first 16 chars of hash + source (native vs redownload-fallback).
  • Compare dirs logs the four bucket counts (local-only / remote-only / differing / same).

Docs

  • New MCP.md — how to register ftproxy-mcp with Claude Desktop; full tool catalog with examples; security notes.
  • New TESTING.md — the four test surfaces (cargo lib, client crate, Vitest, endpoint suite) with run instructions and CI mapping.
  • AUDIT-2026-04-26.md refreshed with a status-delta header marking every sprint-1 item DONE; original audit body preserved as a historical snapshot.
  • SECURITY-AUDIT-2026-04-26.md refreshed — F-MED-1, F-MED-2, F-LOW-1, F-LOW-2, F-LOW-3, F-LOW-4 all marked CLOSED with where they landed.
  • API.md extra table now documents encryption, logon_type, key_path, key_passphrase, plus notes on the password-vs- extra.access_token Dropbox routing.
  • USER-GUIDE.md adds an FTP-family Encryption + Logon Type section, Hard Reload to the keyboard shortcuts.
  • CREDENTIALS.md adds SFTP key-file auth setup + an FTPS section.
  • IMPLEMENTATION.md source map adds ftps.rs, ftproxy-mcp.rs.

Build marker

  • UI build: 2026-04-26 v42-encryption-logon-top-level

[0.7.1] — 2026-04-26 (audit sprint 1 — finish line)

Closes the remaining sprint-1 items from AUDIT-2026-04-26.md plus several gaps surfaced in review.

FTPS visible in the palette + dropdowns

  • Protocol-accent swatch added (color teal #5eead4), tab chip class, Site Form dropdown entry, Connect modal dropdown entry — every UI surface that lists protocols now lists FTPS alongside SFTP/FTP.
  • The Site Form's encryption dropdown shows the right two options when protocol = ftps (Explicit / Implicit) and the standard four when protocol = ftp (auto / explicit / implicit / plain).

Logon Type dropdown — FileZilla parity

  • FTP / FTPS site form now has a Logon Type dropdown:
  • Normal — username + password (default; existing behaviour)
  • Anonymous — auto-fills anonymous / anonymous@example.com on the connect body; bridge::post_connect substitutes
  • Ask for password — clears the stored password; the JS prompts via window.prompt() at connect time
  • SFTP site form has a different Logon Type set:
  • Normal — password
  • Ask for password
  • Key file — file picker for an OpenSSH private key plus an optional passphrase. transport::sftp::connect_with_policy detects extras.logon_type == "key", loads the key via russh_keys::load_secret_key, and authenticates via authenticate_publickey instead of password.
  • connectSaved() honors logon_type: prompts when ask, substitutes anonymous credentials at the bridge layer.

S3 multipart upload (>5 GB)

  • transport::s3::upload switches to CreateMultipartUpload → loop of UploadPart (64 MiB chunks) → CompleteMultipartUpload when data.len() > 5 GB. Falls back to single PutObject for smaller payloads. Best-effort AbortMultipartUpload on failure to avoid leaving billable orphan parts.

OneDrive resumable upload (>4 MiB)

  • transport::onedrive::upload POSTs createUploadSession for files larger than 4 MiB, then PUTs 4 MiB chunks against the session URL with Content-Range headers. Best-effort session DELETE on chunk failure.

F-LOW-3: SFTP TOFU first-run explainer

  • One-time modal on bootstrap (gated by localStorage key ftproxy_sftp_tofu_explained) explaining FTProxy's default TOFU behaviour and how to flip on strict pinning under Settings.

ftproxy-mcp binary — Model Context Protocol server

  • New src-tauri/src/bin/ftproxy-mcp.rs (~310 lines). Speaks JSON-RPC 2.0 over stdio per the MCP spec. Each tool is a thin pass-through to the local bridge:
  • list_remote, list_local, upload, download, mkdir_remote, rename_remote, delete_remote, remote_hash, get_transfers, get_session, list_sites, connect, disconnect, get_logs
  • Resolves the bearer token from <data dir>/token or FTPROXY_TOKEN env var; base URL from FTPROXY_BASE (default http://127.0.0.1:7878).
  • Runs on a per-process tokio current-thread runtime so the stdio loop stays synchronous from MCP's perspective.

Frontend test scaffolding (Vitest + jsdom)

  • package.json gains vitest 1.6 + jsdom 24 devDependencies and npm test / npm test:watch scripts.
  • vitest.config.js overrides the default **/dist/** exclude so tests under dist/__tests__/ are picked up.
  • First test set: dist/__tests__/helpers.test.js — 11 assertions covering extOf, fmtSizeSplit, isSinglePane, and FTPS-mode parsing. Result: 11/11 PASS.

Build marker

  • UI build: 2026-04-26 v35-logon-type-mcp-vitest

[0.7.0] — 2026-04-26 (audit sprint 1: FTPS, security hardening, doc cleanup)

This release closes most of sprint 1 from AUDIT-2026-04-26.md and the high-priority findings from SECURITY-AUDIT-2026-04-26.md.

New protocol — FTPS (FileZilla 4-mode parity)

  • transport/ftps.rs — FTP over TLS via suppaftp's async-rustls feature (already enabled). Both Explicit (AUTH TLS upgrade on port 21) and Implicit (TLS handshake on port 990) modes wired, using a webpki-roots-backed rustls connector.
  • normalize_protocol accepts ftps, ftpes, ftp-tls, ftptls.
  • Encryption mode dropdown in the Site Form for FTP, matching FileZilla's four entries:
  • Use explicit FTP over TLS if available (default — tries TLS, falls back to plain with a warn log)
  • Require explicit FTP over TLS
  • Require implicit FTP over TLS (port 990)
  • Only use plain FTP (insecure) Stored as extra["encryption"]. The bridge dispatches the right transport based on the mode; auto-mode logs whichever path won.
  • New deps: futures-rustls 0.26, rustls 0.23 (ring + std), webpki-roots 1.
  • Added suppaftp deprecated feature for connect_secure_implicit.

Security findings closed

  • F-MED-1: GCS service-account JSON moved to OS keychain. New credentials::SECRET_EXTRA_KEYS list; bridge::post_site / put_site move every listed key out of extra and store it under keychain key <site_id>:<extra_key>. post_connect hydrates from keychain back into the in-memory extra map for the transport. delete_site cleans up. Result: nothing in sites.json is a secret anymore; this matches the rule we already follow for passwords.
  • F-MED-2: Bearer token redacted in tracing logs. Replaced TraceLayer::new_for_http() with a make_span_with builder that strips ?token=… from URIs before logging — WS connect URLs no longer leak the bearer token to stdout / log files / aggregators. 4 unit tests cover the redactor.
  • F-LOW-1: Strict CSP shipped. tauri.conf.json app.security.csp goes from null to default-src 'self' tauri: ipc: …; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self' ipc: http://ipc.localhost http://127.0.0.1:7878 ws://127.0.0.1:7878; script-src 'self'. No external script execution, only Google Fonts CSS, only the local bridge for XHR/WS.
  • F-LOW-2: clippy + cargo-audit added to CI. New lint-and-audit job runs on every push: cargo clippy --lib --tests -- -W clippy::all -A dead_code and cargo audit. Catches new RustSec advisories before they merge.
  • F-LOW-4: Unix file permissions locked to 0o600. Token, sites JSON, bookmarks JSON, known_hosts JSON, app config JSON all call config::lock_user_only() after fs::write. No-op on Windows (NTFS user-ACL covers it).

Per-protocol Verify hash exposure

  • Dropboxremote_hash returns the server's content_hash (Dropbox's custom 4 MB-block SHA-256-based scheme; algo name dropbox or content_hash).
  • Google Driveremote_hash returns md5Checksum from Drive API metadata (algo md5).
  • OneDriveremote_hash returns whichever Graph hash the caller asks for (sha1, sha256, quickxor); auto picks the best available.
  • SFTP / WebDAV fallbackbridge::get_files_remote_hash now re-downloads + hashes locally (md5 / sha256) when the protocol's native primitive returns None. Capped at 256 MiB so a 10 GB blob doesn't silently stream over the network just to get a checksum. Caps blocked hash returns 400 with a pointer to /transfers/verify (uncapped).

Sign-out wiring

  • "Sign out" button next to "Sign in" in the Site Form for both Google Drive and OneDrive. Calls oauth_sign_out Tauri command; drops the keychain entry; updates the status line to "Signed out — keychain entry removed".

Cleanup

  • Deleted dead openKebabMenu JS function (kebab UI removed in v17).
  • Deleted body.theme-light CSS overrides — applyTheme() is dark-locked, so the light tokens were unreachable.
  • Deleted stray ftproxy-run.log at the repo root; added *.log to .gitignore.
  • Archived CODEX.mdtasks/archive/CODEX-initial-overhaul.md.
  • Archived tasks/ui-restyle-progress.mdtasks/archive/ui-restyle-progress-2026-04-24.md.
  • tasks/memory.md updated: 9 protocols (was 6), 96/96 tests (was 74/74), OAuth + secret-extras notes added.
  • IMPLEMENTATION.md source map updated: oauth.rs listed, route count 29 → 40.

Tests — total 96 (was 90, +6 new)

  • bridge::tests::redact_token_* (4 tests covering URI redaction)
  • transport::ftps::tests::mode_parse_defaults
  • transport::ftps::tests::default_ports

Build marker

  • UI build: 2026-04-26 v34-audit-sprint1

Sprint 1 items NOT shipped this pass (deferred to sprint 2)

  • ftproxy-mcp binary (3-day MCP server wrapper) — biggest single strategic unlock per AUDIT §3.1; needs its own session
  • S3 multipart upload (>5 GB)
  • OneDrive resumable upload (>4 MiB)
  • Frontend test scaffolding (Vitest)
  • F-LOW-3: SFTP first-run TOFU explanation modal

[0.6.1] — 2026-04-26 (Drive + OneDrive real OAuth — no more scaffolds)

Wired Google Drive and OneDrive end-to-end. The OAuth2 + PKCE flow, loopback-redirect listener, token persistence, and Drive/Graph REST calls are all live. The earlier 0.6.0 scaffold-only behavior is gone.

OAuth2 + PKCE scaffolding (src-tauri/src/oauth.rs, ~280 lines)

  • pkce_pair() generates a 64-char URL-safe verifier + S256 challenge
  • start_callback_listener() binds an axum-style loopback HTTP listener on a free port, returns (redirect_uri, oneshot_receiver). Single request, then shuts down. 5-minute timeout.
  • exchange_code_for_token() POSTs the auth code + verifier to the provider's token endpoint, returns a StoredToken
  • refresh_if_needed() checks expires_at - now < 60s, refreshes via refresh_token grant when stale
  • Token storage: OS keychain via existing keyring crate, service ai.opensentinel.ftproxy.oauth, key = SavedSite UUID
  • Built-in providers: Google + Microsoft (azure-ad common tenant)
  • 5 unit tests cover PKCE, URL construction, provider resolution, state-nonce shape

Tauri commands

  • oauth_sign_in({ provider, client_id, scopes, key }) — orchestrates the full flow: PKCE pair → loopback listener → opens system browser to authorization URL → captures redirect → exchanges code → persists token → returns expiry. CSRF-guarded by state nonce.
  • oauth_status({ key }) — returns { signedIn, expiresAt, scopes } for the current keychain entry; used to reflect sign-in state when re-opening the Site Form

Google Drive transport (replaces 0.6.0 stub)

  • Drive API v3 via reqwest. Path↔ID translation cache ("/" → "root"); resolves nested paths by walking the folder hierarchy with q=name='X' and 'PARENT' in parents
  • list (page-tokenized), mkdir (folder MIME type), unlink, rename (PATCH name), download (alt=media), upload (multipart/related metadata + content)
  • 401 implicit retry via the refresh_if_needed() call before every op

OneDrive transport (replaces 0.6.0 stub)

  • Microsoft Graph /me/drive — path-based, no id↔path cache needed (/me/drive/root:/foo/bar: URL form)
  • list (with @odata.nextLink pagination), mkdir (folder facet
  • conflictBehavior: fail), unlink, rename, download (/content), upload (PUT to /content, capped at 4 MiB — large files need the resumable upload-session API in a follow-up)

Site Form

  • For gdrive and onedrive: replaced the "OAuth not implemented" warning with a real Sign in with Google / Sign in with Microsoft button that calls oauth_sign_in, opens the system browser, and shows live status: "Signed in. Token expires …"
  • New oauth_key field (auto-set to the SavedSite UUID, read-only), used by the transport to look up the keychain entry on connect
  • On open, the form fetches oauth_status and shows existing sign-in state without requiring a re-auth

Tests — total 90 (was 82)

  • oauth::tests::pkce_pair_round_trip — verifier→S256(verifier)
  • oauth::tests::auth_url_includes_required_params
  • oauth::tests::microsoft_provider_config_resolves
  • oauth::tests::unknown_provider_errors
  • oauth::tests::random_state_is_url_safe
  • transport::gdrive::tests::missing_client_id_message_helpful
  • transport::gdrive::tests::missing_oauth_key_message_helpful
  • transport::onedrive::tests::item_url_root_vs_nested
  • transport::onedrive::tests::url_escape_handles_spaces
  • transport::onedrive::tests::missing_client_id_helpful_error

One-time user setup (no code can avoid this)

  • Google: console.cloud.google.com → enable Drive API → create OAuth credentials of type "Desktop app" → paste client_id into the site's client_id field
  • Microsoft: portal.azure.com → AAD → App registrations → New registration → "Mobile and desktop applications" platform → add http://127.0.0.1 (any port — we bind dynamically) as a redirect URI → grant Files.ReadWrite delegated permission → paste the Application (client) ID into the site

Build marker

  • UI build: 2026-04-26 v31-oauth-drive-onedrive-real

[0.6.0] — 2026-04-26 (cloud-personal: Dropbox, Google Drive, OneDrive)

Three new protocols, raising the supported total from 6 to 9. Each plugs into the same single-pane object-store layout that S3 / Azure / GCS use; switching to one of these tabs collapses the local pane and transit lane and uses the OS file picker / drag-drop for uploads.

Dropbox — full implementation

  • transport/dropbox.rs — Dropbox v2 API via reqwest, personal access token auth (no OAuth dance). Token comes in via extra["access_token"] from the Site Form (or as a fallback in the password field).
  • Implements list (with cursor pagination via list_folder/continue), mkdir (files/create_folder_v2), unlink (files/delete_v2), rename (files/move_v2), download (content.dropboxapi.com/2/ files/download), upload (content.dropboxapi.com/2/files/upload with mode: overwrite), and size (files/get_metadata).
  • Connect-time smoke test against users/get_current_account so a bad token surfaces a clear error before any list call.
  • Path quirk: Dropbox's API uses "" for the root, not "/". The transport translates on every call.
  • Site Form extras panel: dedicated Access token password field with link to dropbox.com/developers/apps.
  • Accent: Dropbox blue #0061ff for swatch + chip + accent-soft retint.

Google Drive — scaffold (OAuth pending)

  • transport/gdrive.rs — Transport trait skeleton; every CRUD method returns a structured error with explicit setup hint pointing to console.cloud.google.com/apis/credentials. Connect itself succeeds so the single-pane layout renders; the wire console then shows the setup hint when listing tries to run.
  • Site Form extras: OAuth client_id field + warning that the flow isn't wired yet.
  • Accent: Google brand yellow #fbbc04.

Microsoft OneDrive — scaffold (OAuth pending)

  • transport/onedrive.rs — same shape as gdrive.rs. Connect succeeds, list / transfer error with hint to portal.azure.com AAD app registration.
  • Site Form extras: client_id + tenant fields.
  • Accent: Microsoft blue #0078d4.

Plumbing changes

  • bridge::normalize_protocol() now accepts dropbox (alias dbx), gdrive (aliases google-drive, googledrive), and onedrive (aliases one-drive, msgraph).
  • post_connect dispatches the three new protocols to their respective Transport::connect constructors.
  • Frontend PROTO_ACCENTS map gets three new entries with proper colors / kicker text / mark badges (DBX / GDR / 1DR).
  • SINGLE_PANE_PROTOS set extended — clicking the new swatches in the palette flips the workspace to single-pane just like S3 / Azure / GCS.
  • Quick Connect modal protocol dropdown lists all 9 protocols; cloud- protocol picks redirect to the Site Form pre-filled with the chosen protocol's extras panel.
  • CSS: chip / swatch styles for the new protocols use their accent colors.

Tests

  • 5 new lib tests, total now 82 passing:
  • transport::dropbox::tests::dbx_path_root_to_empty — verifies Dropbox's ""-is-root quirk
  • transport::dropbox::tests::missing_token_yields_helpful_error
  • transport::gdrive::tests::pending_error_carries_setup_hint
  • transport::onedrive::tests::pending_error_carries_setup_hint
  • bridge::tests::normalize_protocol_cloud_aliases
  • NATIVE_MENU_IDS contract tests still green; no menu change.

Build marker

  • UI build: 2026-04-26 v30-dropbox-gdrive-onedrive

[0.5.3] — 2026-04-24 (UX polish: connect, context menu, branding)

Quick Connect

  • Auto-save to Sites after every successful /session/connect. Dedup by protocol+host+username; password stored in OS keychain via existing /sites POST. Note stamp: Auto-saved from Quick Connect on YYYY-MM-DD.
  • Protocol preset: Connect modal now pre-selects whichever protocol is currently active — live session's protocol if connected, or the user's forced swatch pick. Port defaults per protocol (22 / 21 / 443) and only overwrites if the user hasn't typed a custom value.
  • Connect… / Sites… / Disconnect buttons restored to the action bar so cloud-protocol connections no longer require the native OS menu.
  • Cloud-protocol Quick Connect opens the full Site Form pre-filled with the chosen protocol (instead of dumping the user in Site Manager to click around).

Context menus

  • Right-click was silently brokenopenCtxMenu and openMenu were setting inline left/top on the inner .ctx-menu / .dropdown elements, but only the outer #ctx-root / #dropdown-root has position: fixed. Menus were rendering at (0,0) behind the page. Now both functions set coords on the root.
  • Context menus now mirror the action bar for discovery parity:
  • Local: Open (if dir) · ↑ Upload selected · New folder · Rename · Delete · Compare dirs · Refresh
  • Remote (FTP/SFTP/WebDAV): Open / ↓ Download / Save as… · New folder · Rename · Delete · Verify hash · Compare dirs · Copy remote path · Refresh
  • Remote (S3/Azure/GCS): same minus Compare dirs, plus Copy S3 URI / Copy Azure URL / Copy gs:// URI that writes a real cloud URI (s3://bucket/key, https://acct.blob.core.windows.net/..., gs://bucket/key) to the clipboard
  • New verifyRemote() calls /files/remote/hash?path=&algo=md5.

Palette placement

  • Protocol-accent palette moved from below the shell to directly below the tabs strip and above the shell. No more scrolling to the bottom to preview protocols / trigger the single-pane flip.

Branding

  • Swapped the inline SVG leaf for the actual Go Green Paperless Initiative logo image (dist/assets/GGPI.png, 64px tall, drop-shadow for brand-green glow).
  • Brand row: [GGPI logo] · GoGreen · Transit. Clicking opens gogreenpaperlessinitiative.com in the user's browser.
  • .brand-lockup / .brand-primary / .brand-secondary CSS preserved so the "GoGreen Transit" text still reads alongside the logo.

Logging

  • statusLog entries added for modal opens (Quick Connect, Site Manager, Settings) and for dismissed file-picker dialogs so the Wire console always shows what is happening, not just what transferred.
  • Single-click a directory now walks into it on both local and remote panes, matching modern file-explorer behaviour. Modifier keys (Ctrl / Shift / ⌘) suppress the auto-navigate so multi-select still works. Single-click on .. walks up one directory.
  • navigateLocal(path) / navigateRemote(path) log a cmd wire line (cd local: … / cd remote: …) so every directory change is visible in the Wire console.

Build marker

  • UI build: 2026-04-24 mock-port-v29-nav-logs in Wire console.

[0.5.2] — 2026-04-24 (mock-workspace port, phase 3 — finish line)

The third and final pass on the mock port. The shell structure was right after 0.5.1 but subtle size / color / layout drift kept surfacing in user review. This release closes every known gap, adds the native OS menu the app used to ship with, and introduces layout-per-protocol so object-store connections feel native.

Native OS menubar (restored)

  • src-tauri/src/lib.rs installs a real Windows menubar via Tauri's MenuBuilder / SubmenuBuilder — File · Edit · View · Transfer · Server · Bookmarks · Help. Same pattern as GoGreenMarketing.
  • File → Quick connect (Ctrl+Q) · Site Manager (Ctrl+S) · Import / Export · Exit (native quit).
  • Edit → Settings · Clear queue · native Undo/Redo/Cut/Copy/Paste.
  • View → Refresh both (F5) · Compare dirs · Toggle wire / palette · native Reload (Ctrl+R) / DevTools (Ctrl+Shift+I).
  • Menu events emit menu-action via window.eval(__ftpMenuAction(id)) which routes to the existing JS dispatchers — no @tauri-apps/api npm dependency required.
  • NATIVE_MENU_IDS const + 3 unit tests assert the id contract between Rust and JS never drifts silently.

Layout-per-protocol

  • .workspace.single-pane CSS collapses the grid to 1fr, hides the local pane and transit lane. Triggered when the active protocol (real session or user-forced swatch pick) is S3 / Azure / GCS.
  • openFilePickerUpload() — in single-pane mode, the ↑ Upload action opens an OS file picker and POSTs selected files to /transfers/upload-blob. Drag-drop from the OS still works.
  • Switching tabs flips the layout live without reload.
  • Clicking a protocol swatch in the palette now also toggles the layout, for design preview.

Mock-match drift fixes

  • .wire-body uses height: 200px and max-height: 200px + overflow-y: auto (was max-height alone, which let the box shrink to content height instead of holding 200px).
  • .workspace uses fixed height: 520px so the panes are bounded and .rows actually scrolls inside them (previously unbounded min-height: 460px grew the pane to fit 50+ files).
  • applyTheme() is dark-locked — config.theme no longer leaks a theme-light class that was washing out colours.
  • --accent-sftp corrected from #5eead4 to #7dffb2 (matches mock).
  • .action.disabled is now color: var(--ink-mute) only (no opacity: 0.5) — disabled buttons are still legible.
  • Session-tab strip moved out of .shell-head into its own rounded row directly below the concept-tag, matching the user's requested placement. Tabs no longer wrap inside the shell-head's squeezed center column.
  • Queue summary uses .summary / .done / .fail classes at 11px mono (was .queue-summary at 10px uppercase); separators are mid-dot · at var(--ink-faint); "done" rendered in --ok, "failed" in --err — matches mock verbatim.
  • Statusbar uses inline .ok green dot + .v values with units baked in ("0 KB/s", "— ms"); dropped the .u unit span that was dimming units with var(--ink-faint).
  • Body padding 36px 36px 64px, max-width 1440px on concept-tag / tabs-strip / shell / palette / footnote — matches mock exactly.
  • Added the mock's rise / rowIn / wireIn keyframe animations.
  • Wire-line rows stamped with wire-new class so new entries animate in, matching the mock's streaming effect.

In-app connect paths

  • Connect… · Sites… · Disconnect actions added back to the action bar so cloud-protocol connections don't require opening the OS menu.
  • quickConnect() for S3 / Azure / GCS opens the full Site form pre-filled with the chosen protocol (was dumping the user in Site Manager to click around).

Logging

  • statusLog("sys", "Layout: single-pane (s3)") on every layout flip.
  • statusLog("sys", "Palette accent: <proto>") on swatch click.
  • statusLog("cmd", "menu: <id>") on every native-menu dispatch — visible in the Wire console so you can tell a click fired even if the JS handler no-ops.

Build marker

  • UI build: 2026-04-24 mock-port-v20-final shown in the Wire console on bootstrap so stale WebView2 caches are obvious.

[0.5.1] — 2026-04-24 (mock-workspace port, phase 2)

Phase-1 of the mock port ("editorial reskin") had kept the FileZilla skeleton — same <tr>/<td> lists, same status-pane log, same quickconnect row under the menubar. The visual tokens were right; the structure wasn't. This release finishes the port so the live UI matches dist/mock-workspace.html in both chrome and content shape.

Rewritten renderers in dist/app.js

  • renderFileList now emits <div class="row"> with the mock's structure: glyph (↑ / ▸ / ◆ / ⇢), name (.n + .ext pill + symlink .target), split size + faded unit, local mod column, remote perms (each char coloured .t/.r/.w/.x/.dash), owner (.u / .g), hash (.ok / .mute / .prog).
  • renderSessionTabs now emits .tab with a coloured .chip.<proto> square, a .x close button, and a trailing dashed .tab-add.
  • renderTransfers emits .q-item pills (active / ok / fail) with direction arrow, filename, and status/percent chip.
  • statusLog writes .wire-line.tx/.rx/.sys/.err/.warn rows with [ts] [TAG] [msg] shape; rx pulls the numeric response code into a .code span, tx pulls the first word into .cmd. Wire frame count and chip port update live.
  • New renderPathBar(side, path) — breadcrumb trail with .crumb / .sep, double-click to swap into raw-edit input, Enter to navigate, Esc to cancel.
  • renderSessionMeta() fills the shell-head right column with pulse · live · host · uptime when connected; a rolling tick updates uptime every second.
  • updateModeRail() populates channel / type / AUTH TLS pills from the live session.
  • updateWireMeta() keeps CTRL :<port> and TLS · session · N frames current.

Action bar

  • .action[data-cmd] buttons handle every command formerly on the toolbar + menubar (upload / download / new-folder / rename / delete / verify / compare / sites / connect / disconnect / reconnect / refresh / process-queue / cancel / copy-token).
  • Kebab () opens a dropdown listing File / Edit / View / Transfer / Server / Bookmarks / Help — existing MENUS dispatch unchanged.
  • Connect action opens a focused Quick-Connect modal that writes back to the hidden #qc-* inputs then calls quickConnect().
  • Keyboard shortcuts: U upload, D download, R refresh, F5 refresh both, Esc close dropdowns/menus (existing).

Chrome

  • Drag-over highlight rule .pane.drag-over + pseudo "drop to transfer" label added to dist/styles.css.
  • Hidden #local-path / #remote-path text inputs retained for backwards compat with existing navigateLocal/Remote plumbing; UI now uses the breadcrumb path bar.

Build marker

  • UI build: 2026-04-24 mock-port-v2 — shown in the Wire console on bootstrap so you can tell a fresh bundle loaded.

[0.5.0] — 2026-04-24 (rebrand + transit-console UI)

Brand

  • Product renamed Go Green Transit under the Go Green Paperless Initiative umbrella. Repo, binary name, and crate identifiers keep the ftproxy handle for continuity. productName and window title in tauri.conf.json updated.

Visual system — "Transit Console"

  • Full restyle in dist/styles.css: IBM Plex Sans / Mono with Instrument Serif for editorial accents. Dark theme is now the default; light theme is a tasteful secondary.
  • Protocol-accent identity — a single --accent CSS variable drives the whole UI; applyAccent() in dist/app.js re-tints the chrome per the active session's protocol (SFTP teal, FTP amber, WebDAV purple, S3 orange, Azure cyan, GCS pink). The house Go Green brand green is the default when no session is connected.
  • Brand lockup in the menubar: leaf mark + "GoGreen" wordmark + "TRANSIT" small-caps descriptor. Leaf asset at dist/assets/logo-leaf.svg.
  • Protocol badge next to the brand shows the live protocol mark (SSH / FTP / DAV / S3 / AZ / GCS) and a serif title — updates on session change.
  • Transit lane between local and remote panes: two arrow buttons (↑ upload, ↓ download) bound to the currently selected entries in each pane. Tick rail between them, vertical mono labels at the ends.
  • Queue rows, session tabs, toolbar buttons, quickconnect form, status bar, modals, and dropdowns all restyled to the new system.

Frontend compatibility

  • Every DOM id / class / data-attribute consumed by dist/app.js preserved so the restyle was a CSS+chrome change, not a rewrite. Drag-drop, tree navigation, queue rendering, session tabs, and modals all work as before.

[0.4.0] — 2026-04-23 (cloud storage plugins)

All three cloud storage backends shipped end-to-end: Rust plugin + Site Manager form + connect flow + client-crate types. You supply the access keys; FTProxy handles everything else.

ConnectionInfo.extra

  • ConnectionInfo and the bridge ConnectRequest gained an extra: HashMap<String, String> field.
  • SavedSite.extra (optional, serde-default so existing sites still load) persists protocol-specific settings alongside host/user.
  • post_connect merges extra from the connect body with any stored on the referenced siteId so the UI doesn't have to resend cloud config every time.

S3 transport (transport::s3)

  • Built on aws-sdk-s3 (v1). Probes with HeadBucket on connect.
  • Inputs: username = access key, password = secret access key, extra.bucket (required), extra.region (defaults to us-east-1), extra.endpoint (forces path-style, enabling MinIO, Cloudflare R2, DO Spaces, Wasabi, etc.), extra.path_style override.
  • Operations: list/list_page via ListObjectsV2 with / delimiter (common prefixes become directories); mkdir creates a zero-byte prefix/ marker; rmdir batches DeleteObjects in chunks of 1000; rename = CopyObject + DeleteObject; download/upload + streaming download_to/upload_from; size via HeadObject.
  • Single-part PutObject today; multipart (required for >5 GB) is a follow-up.

Azure Blob transport (transport::azure)

  • Built on azure_storage + azure_storage_blobs. Probes with get_properties on the container.
  • Inputs: username = account name, password = account key, extra.container (required).
  • All operations target BlockBlobs. rename = CopyBlob (server-side)
  • DeleteBlob. list_page currently returns one page per call; Azure's continuation plumbs through the same stream.

GCS transport (transport::gcs)

  • Raw REST against storage.googleapis.com with a service-account JWT exchanged for an OAuth2 access token (cached, refreshed ≥30 s before expiry). Avoids the full google-cloud-storage crate to keep the dep tree tight — only adds jsonwebtoken + time.
  • Inputs: extra.bucket, extra.service_account_json (the whole JSON blob; paths are not supported because the bridge runs in a different data dir than the user's shell).
  • Operations: list_page uses objects.list?delimiter=/; upload uses uploadType=media; rename uses rewriteTo; rmdir iterates + deletes (no batch delete on GCS).

Bridge / routing

  • normalize_protocol accepts s3, azure / azureblob / azure-blob, gcs / gs, plus the existing ftp / sftp / webdav.
  • post_connect dispatches to the right transport constructor.
  • port = 0 is the right default for cloud protocols — it's a sentinel the plugin resolves internally, never a TCP port.

Frontend

  • Protocol dropdown in quickconnect, Site Manager, and Site Form now lists SFTP / FTP / WebDAV / S3 / Azure / GCS.
  • Site Form grows a protocol-aware extras panel: S3 shows bucket + region + endpoint + path_style; Azure shows container + endpoint_suffix; GCS shows bucket + a textarea for the full service-account JSON; WebDAV shows a note about pasting the full base URL as the host.
  • Quickconnect routes S3/Azure/GCS to the Site Manager because those protocols need more than the four-field row can hold.
  • connectSaved now sends the site's stored extra in the connect body so cloud tabs reconnect cleanly.

Client crate

  • opensentinel-ftproxy-client gains ConnectRequest.extra, SavedSite.extra, and NewSite.extra fields. Consumers targeting cloud backends pass the same key/value pairs the bridge expects.

Tests

  • cargo test --lib main: 74/74 passing (was 69).
  • cargo test client crate: 10/10 passing.
  • New transport tests: transport::s3::prefix_and_key_roundtrip, transport::gcs::enc_matches_s3_rules, transport::gcs::key_and_prefix_math, plus extras-parsing coverage on ConnectionInfo.

Deliberately deferred (known gaps, not vaporware)

  • Multipart S3 uploads — single-part is fine up to 5 GB.
  • GCS / Azure true cursor pagination in list_page — current impl streams page-at-a-time but doesn't thread the token back through opts.continuation. Low-priority until someone hits a

    1000-object listing in the UI.

  • Mobile packaging — still intentionally out of scope.

[0.3.1] — 2026-04-23 (close-out pass)

Landed everything the 0.3.0 summary called out as "follow-up" except mobile packaging.

Runtime-resizable concurrency

  • AppState.transfer_slots is now RwLock<Arc<Semaphore>>. PUT /config with a new concurrency value swaps the Arc; in-flight permits stay valid (they reference the prior Arc), new jobs acquire from the fresh semaphore, and a log line records the transition.

Per-session transfers API

  • POST /sessions/:id/transfers/upload and /download route through a new slot_for_session / lock_for_session helper pair. Existing top-level /transfers/* routes still target the active slot so clients can start a long transfer on a background tab without switching to it.
  • transfer_upload / transfer_download were extracted into run_transfer_upload / run_transfer_download functions that take an optional session_id; the old routes delegate with None.

Headless bridge binary

  • src-tauri/src/bin/ftproxy-bridge.rs — runs the full axum router without the Tauri shell. Same token + data dir as the desktop app so the existing tools (test-endpoints.ps1, the OpenSentinel client crate) work against it.
  • lib.rs now exposes build_app_state() + run_headless_bridge() and both Tauri setup and the bin use the same constructor.

GitHub Actions CI

  • .github/workflows/ci.yml:
  • unit-tests — runs cargo test --lib on the main crate plus cargo test --all-features on the client crate.
  • endpoint-tests — spins scripts/docker-compose.test.yml (atmoz/sftp + pure-ftpd), builds + runs ftproxy-bridge, locates the token in the Linux XDG data dir, and executes scripts/test-endpoints.ps1 with FTPROXY_TEST_STACK=docker.
  • test-endpoints.ps1 token lookup is cross-platform: FTPROXY_TOKEN env var wins, then APPDATA, XDG_DATA_HOME, HOME/.local/share, with a final recursive fallback.

WebDAV transport (first real plugin)

  • New transport::webdav module implementing the Transport trait via reqwest + quick-xml. Covers PROPFIND (list + list_page), MKCOL, PUT (buffer-first upload with cancel), GET (streaming download_to with cancel + progress), DELETE, MOVE, HEAD (size), and OPTIONS (connect probe).
  • Namespace-loose multistatus parser handles Apache / Nextcloud / ownCloud / IIS DAV shapes. 5 unit tests cover URL parsing, percent decoding, local-name stripping, and the multistatus parser.
  • Wired into normalize_protocol (webdav / dav) and post_connect. Frontend Quickconnect + Site Manager expose the new protocol in the dropdown.
  • Other object-store backends (S3, Azure Blob, GCS) deliberately not shipped yet — each one needs a distinct credentials UX (access key + secret + region + endpoint override) and a bucket/prefix picker that isn't a plain path bar. WebDAV proves the plugin surface; the other three follow the same code shape when a UX pass lands.

Tests

  • cargo test --lib main crate: 69/69 passing (was 64).
  • cargo test client crate: 10/10 passing.

[0.3.0] — 2026-04-23 (same-day roadmap close)

Completes the backlog (except mobile, which is explicitly deferred). Everything below landed on top of 0.2.0 without breaking the 60 tests it shipped with — the count is now 64 main + 10 client crate.

Multi-session tabs

  • New SessionSlot type holding its own info: RwLock<SessionInfo> and transport: Arc<Mutex<Option<Box<dyn Transport>>>>. AppState now owns sessions: RwLock<Vec<Arc<SessionSlot>>> + active_session_id.
  • Legacy /session/* endpoints redirect to the active slot so existing consumers keep working. New endpoints: GET /sessions, POST /sessions, GET/POST /sessions/active, GET/DELETE /sessions/:id, POST /sessions/:id/disconnect.
  • Frontend: tab strip above the workspace with per-tab close, new-tab button, and a connected/disconnected dot. sessions.changed WS event keeps the strip live.
  • Bridge helpers lock_transport_for(state, id) let future multi-tab transfer routes target a specific tab.

Plugin surface for object storage

  • RemoteEntry now documents a kind = "object" value for S3-like backends; ListPage { entries, next_token } + ListOpts { prefix, continuation, limit } added to the Transport trait with a default impl that returns the whole listing (preserving SFTP/FTP behaviour).
  • New GET /files/remote/page?path=&continuation=&limit= endpoint.
  • transport::plugin submodule reserved as the docs/helpers anchor for future WebDAV/S3/Azure/GCS implementations.

OpenSentinel client crate

  • New crate crates/opensentinel-ftproxy-client/ with typed structs, retry-on-retryable, auto-reconnecting WS event stream, and 10 unit tests. Covers every endpoint the bridge exposes today, including /sessions*, /files/remote/page, /dir/compare, and /transfers/verify.
  • Pure reqwest + tokio-tungstenite + serde — no dependency on the FTProxy crate, so consumers embed just the client.

Wrap-ups

  • SFTP verify fallback: when verifyAfterUpload = true on SFTP and the server has no hash primitive, re-download the file (up to 256 MiB) and compare to the client-side MD5. Over that cap we trust SSH wire integrity and report unverified.
  • Host-key mismatch modal: the bridge now emits hostkey.mismatch with { host, port, message } on strict-mode rejections. The UI shows a modal with the bridge message, a field to paste the newly offered fingerprint, and a "Trust new key" button that calls POST /hostkeys/trust.
  • test-endpoints.ps1 docker switch: set $env:FTPROXY_TEST_STACK = "docker" to point the suite at scripts/docker-compose.test.yml (atmoz/sftp on 22322, demo/demo). The script auto-provisions a temporary site in that mode.

Tests

  • cargo test --lib on the main crate: 64/64 passing (was 60).
  • cargo test on opensentinel-ftproxy-client: 10/10 passing.
  • New handler tests: sessions_list_starts_with_one_active_slot, create_and_switch_sessions.
  • New transport tests: list_page_is_constructible, list_opts_defaults_to_none.

[0.2.0] — 2026-04-23

Roadmap pass: everything in tasks/todo.md "Next" except mobile + the plugin/OpenSentinel-crate scaffolding (still pending, see todo.md).

New Rust modules

  • app_config.rs — server-side config persisted to <data_dir>/config.json with fields: defaultLocalPath, concurrency, logLevel, theme, verifyAfterUpload, ftpTransferType, completionSound, strictHostKey. Loaded at startup, clamped/validated on write.
  • bookmarks.rsBookmark as (site_id?, remote_path, local_path?, note?). Persisted to bookmarks.json. Distinct from SavedSite so one site can have many working directories.
  • known_hosts.rs — SFTP fingerprint store (JSON, not OpenSSH format) with check() returning Unknown | Match | Mismatch { stored }. Used by the new host-key pinning flow.
  • win_import.rs — Windows-only registry reader for WinSCP (HKCU\Software\Martin Prikryl\WinSCP 2\Sessions) and PuTTY (HKCU\Software\SimonTatham\PuTTY\Sessions). Host + port + username only; neither tool stores usable passwords.

Transport trait extensions

  • download_to(remote, writer, cancel, progress) / upload_from(remote, reader, cancel, progress) — streaming variants, 64 KiB chunks, so large transfers don't load the file into memory.
  • size(remote) probe for transfer bytesTotal (SFTP metadata, FTP SIZE).
  • remote_hash(remote, algo) — FTP issues XMD5 / XSHA256 / XCRC via the SITE-like channel; SFTP returns None.
  • ProgressFn callback threaded through the streaming variants.
  • tokio::sync::watch::Receiver<bool> cancel flag; the worker checks it between chunks.

Host-key pinning (TOFU → strict)

  • HostKeyPolicy::{AcceptAny, Tofu}. SftpTransport::connect_with_policy records the SeenKey (algorithm + fingerprint + verdict) from russh's check_server_key.
  • On first successful SFTP connect, the fingerprint is upserted into known_hosts.json. A subsequent connect with a different key is rejected at the TLS layer (russh returns false from the handler).
  • hostkey.seen WS event fires on every connect with { host, port, algorithm, fingerprint, new }.
  • Settings toggle: "SFTP host-key verification" (off = legacy accept-any, on = strict). A caller can force per-connect override with acceptAnyHostKey: true in the connect body.

New endpoints

  • GET /config — server-side settings.
  • PUT /config — validate + persist + broadcast config.changed.
  • GET /bookmarks, POST /bookmarks, PUT /bookmarks/:id, DELETE /bookmarks/:id — full CRUD.
  • GET /hostkeys, POST /hostkeys/trust, DELETE /hostkeys/:host/:port.
  • POST /dir/compare { localPath, remotePath, maxDepth } — recursive classifier; returns { localOnly, remoteOnly, differing, same }.
  • POST /transfers/verify { localPath, remotePath } — hashes local file and compares to FTP HASH/XMD5/XSHA256/XCRC response.

Transfer pipeline

  • transfer_download / transfer_upload now call the streaming variants and emit transfer.progress WS events throttled to 250 ms.
  • Cooperative cancel: DELETE /transfers/:id signals the worker via a watch::Sender<bool> stored in AppState.cancel_flags. The worker short-circuits between chunks, surfaces a transfer.cancelled event, and cleans up the partial file for downloads.
  • transfer_slots: Arc<Semaphore> gates concurrent transfers to AppConfig.concurrency (default 2, clamp 1–16).
  • Optional post-upload integrity verify: when config.verifyAfterUpload is on, the worker computes local MD5/SHA-256/CRC32, asks the server for its own hash, and attaches verified: true|false|null to the response.

Frontend

  • Dark mode with a new theme-dark body class and a three-way Settings toggle (light / dark / auto). "Auto" follows prefers-color-scheme.
  • Settings modal now reads/writes /config on the server (not just localStorage). Adds strict-host-key toggle and log-level chooser.
  • Bookmarks menu: Add current path, Manage bookmarks (list + jump
  • delete), Import from WinSCP, Import from PuTTY.
  • First-run import prompt: on launch, if WinSCP or PuTTY sessions exist in the registry and the sites list is empty, offer to import.
  • Live progress bar on every running queue row, updated from transfer.progress events. Per-row cancel button (✕) issues DELETE /transfers/:id.
  • Directory sync modal (View → Compare directories) uses POST /dir/compare and shows classified results in a tabbed preview with buttons to apply each direction.
  • UI build marker bumped so it's obvious which pass is live.

Infra

  • scripts/docker-compose.test.yml brings up atmoz/sftp on 22322 and stilliard/pure-ftpd on 22321, both bound to 127.0.0.1 with demo/demo creds so the endpoint suite can run without the IONOS host.
  • Cargo deps added: md-5, sha2, crc32fast, hex, base64, urlencoding, and (Windows-only) winreg.

Tests

  • 60 unit tests (was 45). Additions: app_config, bookmarks, known_hosts, win_import::decode_key_roundtrip, transport::sftp::host_key_policy_variants_compile.

Still pending (explicitly deferred, not abandoned)

  • Multi-session tabs — big AppState refactor (would invalidate the /session/* contract for external consumers). Tracked in todo.md.
  • Plugin surface for S3/WebDAV/GCS — RemoteEntry generalization.
  • Standalone opensentinel-ftproxy-client crate (typed client + WS reconnect).
  • Mobile packaging — deliberately out of scope per this pass.

[0.1.0] — 2026-04-22

First functional release. Everything below was built in a single session on top of an empty Tauri scaffold.

Backend (Rust, Tauri 2)

  • Replaced the initial raw TcpListener HTTP toy with an axum + tokio server. 29 routes, bearer-token middleware, CORS, WS.
  • Transport trait with two production impls:
  • SFTP via russh 0.45 + russh-sftp 2.1 (pure-Rust, async). Upload uses open_with_flags(CREATE | WRITE | TRUNCATE) (OpenSSH's sftp-server rejects the flags create() emits).
  • FTP via suppaftp 6 with async + async-rustls features. Uses retr_as_stream / finalize_retr_stream and put_file with futures_util::io::Cursor.
  • Localhost bind, preferred port 7878 with fallback to an OS-assigned ephemeral port (FTPROXY_PORT env override).
  • Bearer-token auth on every endpoint except /health. Token generated on first launch, written to the app data dir.
  • bridge.url discovery file so external local consumers can find the bridge even when it falls back to a non-default port.
  • JSON-persisted saved sites in sites.json.
  • Passwords stored in the OS keychain via keyring 3 with the windows-native (+ apple-native, sync-secret-service) feature flags explicitly enabled.
  • WebSocket /events with typed events: hello, session.changed, remote.changed, transfer.started/completed/failed, sites.changed, log.
  • Structured error envelope: { code, message, retryable }.

Frontend (static HTML/CSS/JS in dist/)

  • FileZilla-style layout: menu bar (File/Edit/View/Transfer/Server/ Bookmarks/Help), toolbar, quickconnect row, scrolling message log, dual panes (each with path bar + tree + file list + foot), queue table, queue tabs (Queued/Failed/Successful), status bar.
  • All menu items and toolbar buttons are wired to real handlers — no stubs left.
  • Working: drag-drop (OS→remote, local→remote, remote→local), right-click context menus, double-click navigation/transfer, breadcrumb navigation, sortable columns, multi-select, per-pane tree lazy-load.
  • Site Manager: full CRUD with a password eye-toggle that fetches the cleartext value from the OS keychain via GET /sites/:id/password.
  • Settings modal (localStorage-backed preferences).
  • FileZilla XML import: File → Import sites from FileZilla XML — reads sitemanager.xml, base64-decodes <Pass> entries, maps <Protocol> integers, and batch-posts to /sites.
  • Directory comparison (View → Compare directories): diffs the current local vs remote pane and reports local-only / remote-only / differing-size.
  • About + bridge info modals with token copy.
  • Live events: WS-driven auto-refresh of remote/local panes on transfer-complete, of queue on any transfer event, of site list on sites.changed.
  • Connection chip in the menu bar — green when connected, red when not — plus toolbar disconnect/reconnect buttons that enable/disable by state.
  • Remote pane clears on every transition to disconnected (startup, explicit disconnect, server drop).

Security / hygiene

  • tauri.conf.json sets dragDropEnabled: false so the webview handles HTML5 drag-drop natively (Tauri's native interceptor blocks drop events otherwise).
  • Loopback bind only. Bearer token required on all non-/health endpoints. Passwords never persist to disk in cleartext.
  • .gitignore covers src-tauri/target, src-tauri/gen/schemas, IDE junk, tasks/.

Infrastructure

  • Provisioned an SFTP account on the IONOS VPS:
  • User gogreensuites_ftp chrooted to /srv/sftp/gogreensuites.
  • /var/www/gogreensuites.com bind-mounted into the jail via /etc/fstab.
  • Docroot set to root:deploy 2775 so group members can create files without violating sshd's chroot ownership rules.
  • Shell /usr/sbin/nologin — SFTP only.
  • Saved as the first GoGreenSuites SFTP entry in the Site Manager with the password in Windows Credential Manager.

Validation

  • 45 unit tests across auth, bridge, config, errors, localfs, persist, state, transport — all green with cargo test --lib. Handler-level assertions use tower::ServiceExt::oneshot so the real router runs without a listener.
  • scripts/test-endpoints.ps1 — 25 end-to-end endpoint assertions, all passing against the live IONOS SFTP server.
  • cargo check clean (3 benign dead-code warnings).
  • Upload integrity spot-checked: local md5sum matches remote md5sum bit-for-bit.

Observability

  • tracing_subscriber + tower_http::TraceLayer wired. Every HTTP request now emits a span with method, URI, status, and duration.
  • #[tracing::instrument] on post_connect, transfer_upload, transfer_download with host/port/user/path fields.
  • RUST_LOG=debug opens up internals; default filter keeps russh and suppaftp at warn so SFTP session chatter doesn't drown signal.
  • User-visible log pane fed by the log WebSocket event; last 1000 entries also available via GET /logs.

Working notes from this session

See tasks/lessons.md for gotchas captured along the way (russh Handler lifetimes, keyring 3.x feature flags, Tauri's dragDropEnabled, and more).