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¶
SiteScheduleandBatchJobgainstart_at/end_at(Unix epoch seconds, optional). Scheduler tick gates firings outside the window.start_atlets users say "fire weekly starting May 5";end_atlets 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-syncfor the "I want to sync 1–2 specific files, not a whole folder" case. TakessiteId,direction,localDir,remoteDir,files[], optionalpolicy. 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
schedulebecomes a 1-step Job named "Schedule for <site>", then the inlinesite.scheduleis cleared. Idempotent — re-running on a fully-migrated config is a no-op. Wired inlib::run_headless_bridgeand 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)¶
BatchStepenum'srename_all = "camelCase"only renamed variant tags (Sync→sync), not the fields of each variant. The frontend was sendingsiteId/localPathbut Rust expectedsite_id/local_path. Fixed by addingrename_all = "camelCase"to each variant individually. Newfile_sync_step_serializes_with_camelcase_fieldstest locks down the wire shape.
Logging¶
crate::batch_jobs::runnowtracing::instrument'd withjob_idin the span.- Per-step
run_stepinstrumented withjob+kindfields. - File-sync step emits start/finish info-level logs +
state.push_logfor the visible log pane. - Migration logs each site → job conversion at INFO with
site_id,sitename, newjob_id,cron.
Tests — net +5 over v0.8.8¶
batch_jobs::tests::file_sync_step_serializes_with_camelcase_fields— assertssiteId/localDir/remoteDiron the wire and round-trips back through Deserialize.batch_jobs::tests::job_with_schedule_window_serializes_camelcase— assertsscheduleStartAt/scheduleEndAtshape and thatNoneskip-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_variantsextended to cover the newfile-syncvariant.- 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 ofGET /sessionsso the user's tab strip never flickers when cron fires.ConnectRequestandDirSyncBodynow accept an optionalsessionIdfield. When set, the connect/sync targets that slot instead of the active one and does NOT switchactive_session_id.scheduler::fire_schedulecreates 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 viastate::push_log. - Channels (env-driven, read at call time so
.envedits don't need a recompile): - Slack —
SLACK_WEBHOOK_URL({ "text": ... }payload) - Discord —
DISCORD_WEBHOOK_URL({ "content": ... }payload) - Telegram —
TELEGRAM_BOT_TOKEN+TELEGRAM_CHAT_ID(Bot APIsendMessage) - Webhook —
WEBHOOK_ON_SUCCESS_URL/WEBHOOK_ON_FAILURE_URL(generic JSON POST with full run record) - Email —
SMTP_*env recognized, but the in-process sender is a stub (would need addinglettreto 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 theDeliveryResultenvelope (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/syncagainst 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_historywithbatchIdset so the cross-site Job History view groups them). - Bridge endpoints:
GET /batch-jobs— listGET /batch-jobs/:id— onePOST /batch-jobs— createPUT /batch-jobs/:id— updateDELETE /batch-jobs/:id— deletePOST /batch-jobs/:id/run— trigger now (returns RunSummary)- Scheduler integration: each tick now also walks
BatchJobs with a populatedscheduleCronand fires those due in the same window. schedule_history::record_finish_with_batchoverload so batch steps stamp the run rows withbatch_id. The free-floatingrecord_finishkeeps 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.ScheduleRuncarries: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(schedulerfor cron,manualfor 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_tscutoff filter - bounded cap behavior. Tests serialize via a
TEST_LOCKso the shared on-disk file doesn't race.
Scheduler wired to record every firing¶
scheduler::fire_schedulenow returnsanyhow::Result<ScheduleRunStats>instead ofResult<()>. It parses/dir/sync's response envelope to extract uploaded / downloaded / failed counts.scheduler::scheduler_loopcallsrecord_startimmediately before each fire (status=running), andrecord_finishafter withsucceeded+ stats orfailed+ error string.triggered_byset to"scheduler"for cron firings,"manual"forPOST /scheduler/run-nowinvocations.
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. Defaultlimit=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_routerand emittracing::infoon 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=20and 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 menuinstall_native_menuand 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.envseeded 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 fromPROTO_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-inLOCAL(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 protocolsunfiltered or5 of 41 siteswhen 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 viaoauth_statusTauri 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_ACCENTSentries fordropbox-local/gdrive-local/onedrive-localwith marksDBX·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. applyAccentlookup 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_FILEpath) — 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.rsroute 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_secretssecret_extras_endpoint_404s_for_unknown_site- Test count: 177 unit + 16 integration (was 173 + 12 at start of v0.8.6 work).
Misc fixes¶
transportmodule is nowpub modso integration tests intests/can constructConnectionInfoand call transportconnect()directly (mirrors whatplaces::cloud_sync_foldersand friends already do).places.rs::cloud_sync_foldersGoogle-Drive detection:<drive>:(no trailing backslash) was being treated byPathBuf::joinas "current dir on G:" rather than the drive root, producingG:My Driveinstead ofG:\My Drive. Normalize trailing separator before join.oauth.rs::load()no longer silently returnsNoneon keyring failures. Logstracing::warn!on entry-build / read errors / JSON corruption (still treatsNoEntryas quiet, since "user hasn't signed in yet" isn't an error).- Removed the stray
let _ = (); // marker for clarityfrom the refresh-decision branch inoauth.rs. - 20 dead-code warnings → 0 (
cargo fixremoved 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 toprovider_config()(auth + token URLs +token_access_type=offlineextra). Threadedclient_secretthrough the whole flow so confidential clients (Dropbox; Google "web" / installed app types) can authenticate. Made PKCE conditional via apkce: boolfield onProviderConfig— Dropbox returnsinvalid_response_typeif PKCE is sent when the app's "Allow public clients" toggle is off, so we omitcode_challengefor Dropbox and send it for Google + Microsoft.save()now stripsaccess_tokenbefore 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 byrefresh_if_needed.refresh_if_needed()triggers when access_token is missing OR within 60s of expiry.- Bridge
post_connect: fordropbox/gdrive/onedrivesites with a stored OAuth identity, callsrefresh_if_neededand injects the fresh access_token asinfo.passwordautomatically. Transport stays oblivious. - New endpoint
GET /sites/:id/secret-extrasreturns keychain-stored extras (client_secret, service_account_json) so the Site Form can pre-populate masked fields on edit. client_secretadded tocredentials::SECRET_EXTRA_KEYS— never serialized tosites.jsonplaintext.
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.jsonon Mac/Linux) → finds custom Dropbox sync folders even when the user moved them off C:. - Reads OneDrive registry (
HKCU\Software\Microsoft\OneDrive\ Accounts\*\UserFolderviawinreg) → 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>-localwhen local mode is selected; on edit, recognizes the-localsuffix 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-filesprotocol. 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 toSmbTransport. 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-filesso 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¶
applyAccentandactiveProtoreordered: 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.applyForcedAccentclears_forceProtoafter 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 viadefault_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_browseron Windows now usesrundll32 url.dll,FileProtocolHandlerinstead ofcmd /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 noresponse_typeleft. rundll32 receives the URL as a single argument and dispatches via Windows shell URL handling, preserving the full string. Lesson saved totasks/lessons.mdand~/.claude/projects/.../memory/.
OAuth callback fixed-port + redirect URI¶
oauth::start_callback_listenernow binds on port 53682 (the convention gcloud uses for desktop OAuth flows) before falling back to a random port. The redirect URI useslocalhost(not 127.0.0.1) because Dropbox treatshttp://localhostas a wildcard for any port when whitelisting redirect URIs buthttp://127.0.0.1literally.- Users register
http://localhost:53682/oauth/callbackonce in their Dropbox app's Redirect URIs. Google + Microsoft honor the loopback wildcard so justhttp://localhostregistration works for those.
Test additions¶
tests/s3_multipart_resume.rs(gatedS3_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 macOSInfo.plistdocument.wflowvia theplistcrate, and validating the Linux.desktopentry against FreeDesktop spec keys.tests/updater_check.rs— 3 cases againsttauri_plugin_updater:: RemoteRelease: spins a localhost axum server, deserializes alatest.jsonpayload, validatesdownload_url()+signature()per target, and asserts version-comparison logic both up and down.- New
oauth.rstests: Dropbox provider config (offline, no PKCE), PKCE conditional in auth URL build, access_token strip insave, refresh-decision branches. - New
places.rstests: Dropbox info.json parsing (personal / business / malformed JSON degrades to None), OneDrive registry smoke, cloud_sync_folders return type. - New
bridge.rstests: localcloud + azure-files innormalize_ protocol, Azure Files SMB-shape translation guard, every supported protocol covered bydefault_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 returningNone— operators had no way to triage.bridge.rsOAuth 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.rsmodule installsmetrics_exporter_prometheusrecorder at startup (idempotent). Exposes/metricstext/plain scrape body — pre-auth (loopback-only deployment makes this safe). - HTTP middleware records
ftproxy_http_requests_total{method, path, status}counter andftproxy_http_request_duration_secondshistogram 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, andftproxy_transfer_duration_secondsper transfer. - Queue and session gauges (
ftproxy_queue_depth,ftproxy_sessions_connected) updated on every/healthpoll — Grafana sees them at the scrape cadence.
JSON access logs (opt-in)¶
RUST_LOG_FORMAT=jsonswitchestracing-subscriberto its JSON formatter so headless deploys can ship logs cleanly into Loki / ELK / Datadog without an intermediate parser.prettyandfullalso accepted; default stays human-readable so desktop dev experience is unchanged.tracing-subscribergains thejsonfeature.
Per-token rate limiting¶
- New
rate_limit.rs—governortoken-bucket keyed on the Authorization header. Default 600 req/min (10/s with a 100-rps burst window) per token, configurable viaAppConfig.rate_limit_per_minute(0disables). /health,/metrics,/eventsare bypassed so liveness probes and Prometheus scrapers can poll freely.- 429s are counted in the metrics counter alongside 200s.
PUT /configclears 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. degradedfires 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.rsgets an in-process LRU (60 s TTL, 256-entry cap) that wraps everykeyring::Entry::get_passwordcall. Repeated reads for the same site within a short window now skip the OS keychain round-trip.setandremovewrite 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 viastate.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.
/healthpolls and/metricsscrapes 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 --lib→ 155/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.jsonpersistence 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 BOTHGoGreenVerifyfrontend ANDGoGreenVerify-APIbackend),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 sftpusersblock 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/fstabso 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(notreload 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'sremotePathis 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:rootwith 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.jsonor$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 getread_only: true, the bridge refuses PUT/DELETE on them with a clear "managed by IT" message, and the user-sidesites.jsonnever 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_offsetimpl on FTP transport — sendsREST <offset>beforeRETR. 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.skippedto 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 anyreqwest::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).socksreqwest feature added sosocks5://user:pass@host:1080URLs work alongsidehttp://.../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
.logfile. 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 --lib→ 142/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'sschedule.cronagainst the (last-tick, now] window, fires a/dir/syncfor 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 dowwith*and comma-lists. Thecroncrate is wrapped to add a synthetic seconds field so it agrees with the user-facing 5-field surface. - Headless
ftproxy-bridgeruns the same scheduler — Windows Task Scheduler can fire a backup at 3 AM with the desktop closed. - New
POST /dir/syncendpoint — 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 commandopen_remote_in_editordownloads to%TEMP%/ftproxy-edit/<sha8>/<basename>, launches the OS default editor (cmd /C start ""/open/xdg-open), and spawns anotifycrate 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¶
SessionSlotnow caches the last successfulConnectionInfo. Newdispatch_connect()rebuilds the transport from any cached info; newreconnect_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. HonorsAppConfig.auto_reconnect(default on); never retries on cancellation or auth failure.- New
POST /sessions/:id/reconnectendpoint 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 cheaplist("."). 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 callsListMultipartUploadsfirst. If a pending upload for this key exists (from a previous interrupted run), it pulls the already-uploaded parts viaListParts, carries their etags forward, and continues from the next part_number — no bytes re-sent, no double-billing on dangling part charges.UploadPartfailures no longer auto-abort — the upload-id is left pending so the next call resumes. NewS3Transport::abort_pending_uploads(remote)for explicit discard-and-restart.CompletedMultipartUploadparts are sorted by part_number before the finalCompletecall so resumed-then-extended uploads serialize correctly.
macOS / Linux Send To equivalents¶
- Renamed conceptually from "Send To shortcut" to platform-appropriate surfaces:
- Windows: existing
.lnkin%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" runsftproxy-cliagainst each selected path. Menu refresh viapbs -flush. - Linux:
.desktopfile in~/.local/share/applications/withMimeType=application/octet-stream;text/plain;…so Nautilus/Dolphin/Thunar surface "Open With → Go Green Transit" on every file. Best-effortupdate-desktop-databaserefresh. - Module renamed from "Windows-only" docstring; Tauri command works on every platform.
Auto-updater (Tauri plugin)¶
tauri-plugin-updater 2wired into the Tauri builder. Newcheck_for_updatesTauri command surfaces an Update / "you're on the latest build" prompt via Help → Check for updates…tauri.conf.jsoncarries 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 fromtauri-cli signer generate, updates flow.capabilities/default.jsongrantsupdater:default.
Code signing / notarization config¶
- New
SIGNING.mdwalks 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.versionbumped to0.8.3.
Tests¶
cargo test --lib→ 147/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.rsmodule + 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, callsplaces::map_network_driveto mount the share via the OS's native SMB stack; every file op (list,mkdir,download_to,upload_from, etc.) runs throughtokio::fsagainst the mount root. Onclose(), 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 bareserverwhen theshareextra is set. Anonymous connections work when username/password are blank. normalize_protocolacceptssmb/cifs/sambaaliases.- Bandwidth throttle is wired into SMB's streaming download/upload paths just like every other transport.
Tests¶
cargo test --lib→ 139/139 (was 134). New tests cover:- Places: smoke test for drive enumeration + quick-locations presence.
- SMB transport: pre-mounted-path attach (no real
net usecall, 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.jsonis now updated on every push / terminal-state change. On restart, anything that wasrunning/pending/queuedbecomesinterruptedwithbytesResumedarmed 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
OverwritePolicyenum onTransferJob:Skip/Overwrite/OverwriteIfNewer/Resume/Rename. - Bridge accepts
policyon/transfers/uploadand/transfers/download. Falls back to AppConfigdefaultOverwritePolicywhen missing. - New
Transport::download_to_offset(remote, w, offset, …)trait method. SFTP overrides with a realseek+streamimpl; everything else uses the defaultdownload_tofallback.
Conflict resolution UX¶
- Server-side handlers now check existence + mtime before kicking off
a transfer.
OverwriteIfNewercompares the local file'smodified()to the remoteRemoteEntry.modified_at;Renamefinds a free.N-suffixed name;Skipshort-circuits to statusskipped;Resumearmsbytes_resumed. Emitstransfer.skippedon the WebSocket event stream.
Bandwidth throttle (real, not a placeholder)¶
- New
throttle.rsmodule.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, andpath_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; honorsFTPROXY_BASE/FTPROXY_TOKENenv 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.lnkin%APPDATA%\Microsoft\Windows\SendTo\pointing atftproxy-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/execendpoint runs a one-shot command on the active SFTP session viarussh'sexecchannel. Captures stdout / stderr / exit status, capped at 1 MiB. Server menu → "Run command…". Transport::run_commandtrait method (default: bail with "SFTP-only"). SFTP override threads throughchannel_open_session/exec/wait.
Speed-test / benchmark¶
- New
POST /benchmarkendpoint 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.xmlfavorite, surfaces directory hierarchy as thefolderfield. - CuteFTP: registry import for CuteFTP 7 / 8 / 9 under
HKCU\Software\GlobalSCAPE\CuteFTP {N}\Sites. Supports nested site folders → flattens toFolder1 / 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 --lib→ 134/134 (was 111). New tests cover:- OverwritePolicy wire round-trip + unknown-fallback.
- TransferJob back-compat deserialize (no
policy/bytesResumedfields). - 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 sync —
SavedSite.scheduleandSiteScheduleare 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 infrastructure —
dist/i18n/{en,es,fr}.json+ at()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 andbytesResumed; 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_pagenow readsNextMarkeroff theListBlobsResponseand threads any caller-suppliedListOpts.continuationback into the request viaListBlobsBuilder::marker. Containers with >1000 blobs paginate correctly; the bridge surfaces the marker as the opaquenext_tokenin theListPageenvelope. - GCS:
list_pagealready threadedpageTokenon the request side, but the response struct (ListResp.next_page_token) had no#[serde(rename_all = "camelCase")]and so never matched GCS'snextPageTokenwire 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 →
Nonecoalescing 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 + GCSlist_pagewithentriescount andhas_nextflag atdebug. 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:
nextPageTokenJSON decode + empty-token coalesce. cargo test --lib→ 111/111 (was 107).npm test(Vitest) → 39/39 (unchanged — pure-backend change).
Doc cleanup¶
tasks/todo.mdrewritten to reflect 0.7.4. The "Known gaps" section is now down to one item (mobile packaging, deferred).tasks/future.mdrewritten — 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.mdupdated: Target A (MCP server) marked shipped viasrc-tauri/src/bin/ftproxy-mcp.rs.AUDIT-2026-04-26.mdhead 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…andImport 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 fromHKCU\Software\FTPware\CoreFTP\Sitesand the free-edition…\CoreFTPLE\Sitesroot, mapping thePTypefield (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_puttyids are gone, replaced byfile_import_winscp,file_import_putty,file_import_coreftp.NATIVE_MENU_IDSand the__ftpMenuActionswitch are kept in lockstep; new Vitest contract test pins the JS side. importFromRegistry(source)is now table-driven via aREGISTRY_IMPORTSmap so adding the next client is a one-line diff in JS plus a Tauri command in Rust.
Logging¶
tracing::instrumentonwinscp_sites,putty_sites, and the newcoreftp_sites. Each emitsinfowith the scanned-count ordebugwhen 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 returnNone. A no-op smoke test confirmscoreftp_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__ftpMenuActionrecognises the newfile_import_*ids and not the oldbookmarks_import_*ones.
Test totals¶
cargo test --lib→ 107/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
extramap 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 hashandCompare dirsbuttons 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-sepdividers collapse with them.actionPolicy(cmd, proto)is the single source of truth for enabled / disabled / hidden / tooltip per cell — implements theAUDIT-2026-04-26.md§9 matrix end-to-end.updateToolbarState()runs synchronously insideapplyAccent()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 · SFTPinstead ofDisconnectedso the chrome looks intentional rather than empty.
Native menu¶
View → Hard Reload (Ctrl+Shift+R)added — clearscaches.keys()sessionStoragebeforelocation.reload(). Same pattern as GoGreenMarketing. Fixes the stale-WebView2-cache class of bug permanently.
Tests¶
cargo test --lib→ 104/104 (was 96; +8 new: FTPS aliases, HashQuery deserialisation, redownload cap constant, StoredToken serde roundtrip, pkce-pair-uniqueness, credentials helpers).npm test→ 30/30 (was 11; +19 indist/__tests__/action-policy.test.jscovering 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 registerftproxy-mcpwith 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.mdrefreshed with a status-delta header marking every sprint-1 item DONE; original audit body preserved as a historical snapshot.SECURITY-AUDIT-2026-04-26.mdrefreshed — 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.mdextratable now documentsencryption,logon_type,key_path,key_passphrase, plus notes on thepassword-vs-extra.access_tokenDropbox routing.USER-GUIDE.mdadds an FTP-family Encryption + Logon Type section, Hard Reload to the keyboard shortcuts.CREDENTIALS.mdadds SFTP key-file auth setup + an FTPS section.IMPLEMENTATION.mdsource map addsftps.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.comon 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_policydetectsextras.logon_type == "key", loads the key viarussh_keys::load_secret_key, and authenticates viaauthenticate_publickeyinstead of password. connectSaved()honorslogon_type: prompts whenask, substitutes anonymous credentials at the bridge layer.
S3 multipart upload (>5 GB)¶
transport::s3::uploadswitches toCreateMultipartUpload→ loop ofUploadPart(64 MiB chunks) →CompleteMultipartUploadwhendata.len() > 5 GB. Falls back to single PutObject for smaller payloads. Best-effortAbortMultipartUploadon failure to avoid leaving billable orphan parts.
OneDrive resumable upload (>4 MiB)¶
transport::onedrive::uploadPOSTscreateUploadSessionfor files larger than 4 MiB, then PUTs 4 MiB chunks against the session URL withContent-Rangeheaders. Best-effort session DELETE on chunk failure.
F-LOW-3: SFTP TOFU first-run explainer¶
- One-time modal on bootstrap (gated by
localStoragekeyftproxy_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>/tokenorFTPROXY_TOKENenv var; base URL fromFTPROXY_BASE(defaulthttp://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.jsongainsvitest 1.6+jsdom 24devDependencies andnpm test/npm test:watchscripts.vitest.config.jsoverrides the default**/dist/**exclude so tests underdist/__tests__/are picked up.- First test set:
dist/__tests__/helpers.test.js— 11 assertions coveringextOf,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 viasuppaftp's async-rustls feature (already enabled). Both Explicit (AUTH TLSupgrade on port 21) and Implicit (TLS handshake on port 990) modes wired, using awebpki-roots-backed rustls connector.normalize_protocolacceptsftps,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
deprecatedfeature forconnect_secure_implicit.
Security findings closed¶
- F-MED-1: GCS service-account JSON moved to OS keychain. New
credentials::SECRET_EXTRA_KEYSlist;bridge::post_site/put_sitemove every listed key out ofextraand store it under keychain key<site_id>:<extra_key>.post_connecthydrates from keychain back into the in-memoryextramap for the transport.delete_sitecleans up. Result: nothing insites.jsonis 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 amake_span_withbuilder 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.jsonapp.security.cspgoes fromnulltodefault-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-auditjob runs on every push:cargo clippy --lib --tests -- -W clippy::all -A dead_codeandcargo 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()afterfs::write. No-op on Windows (NTFS user-ACL covers it).
Per-protocol Verify hash exposure¶
- Dropbox —
remote_hashreturns the server'scontent_hash(Dropbox's custom 4 MB-block SHA-256-based scheme; algo namedropboxorcontent_hash). - Google Drive —
remote_hashreturnsmd5Checksumfrom Drive API metadata (algomd5). - OneDrive —
remote_hashreturns whichever Graph hash the caller asks for (sha1,sha256,quickxor);autopicks the best available. - SFTP / WebDAV fallback —
bridge::get_files_remote_hashnow re-downloads + hashes locally (md5 / sha256) when the protocol's native primitive returnsNone. 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_outTauri command; drops the keychain entry; updates the status line to "Signed out — keychain entry removed".
Cleanup¶
- Deleted dead
openKebabMenuJS function (kebab UI removed in v17). - Deleted
body.theme-lightCSS overrides —applyTheme()is dark-locked, so the light tokens were unreachable. - Deleted stray
ftproxy-run.logat the repo root; added*.logto.gitignore. - Archived
CODEX.md→tasks/archive/CODEX-initial-overhaul.md. - Archived
tasks/ui-restyle-progress.md→tasks/archive/ui-restyle-progress-2026-04-24.md. tasks/memory.mdupdated: 9 protocols (was 6), 96/96 tests (was 74/74), OAuth + secret-extras notes added.IMPLEMENTATION.mdsource map updated:oauth.rslisted, 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_defaultstransport::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-mcpbinary (3-day MCP server wrapper) — biggest single strategic unlock perAUDIT §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 challengestart_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 aStoredTokenrefresh_if_needed()checksexpires_at - now < 60s, refreshes viarefresh_tokengrant when stale- Token storage: OS keychain via existing
keyringcrate, serviceai.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 bystatenonce.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 withq=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.nextLinkpagination),mkdir(folderfacetconflictBehavior: 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
gdriveandonedrive: replaced the "OAuth not implemented" warning with a real Sign in with Google / Sign in with Microsoft button that callsoauth_sign_in, opens the system browser, and shows live status: "Signed in. Token expires …" - New
oauth_keyfield (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_statusand 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_paramsoauth::tests::microsoft_provider_config_resolvesoauth::tests::unknown_provider_errorsoauth::tests::random_state_is_url_safetransport::gdrive::tests::missing_client_id_message_helpfultransport::gdrive::tests::missing_oauth_key_message_helpfultransport::onedrive::tests::item_url_root_vs_nestedtransport::onedrive::tests::url_escape_handles_spacestransport::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_idfield - 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 → grantFiles.ReadWritedelegated 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 viareqwest, personal access token auth (no OAuth dance). Token comes in viaextra["access_token"]from the Site Form (or as a fallback in thepasswordfield).- Implements
list(with cursor pagination vialist_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/uploadwithmode: overwrite), andsize(files/get_metadata). - Connect-time smoke test against
users/get_current_accountso 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 tokenpassword field with link todropbox.com/developers/apps. - Accent: Dropbox blue
#0061fffor 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 toconsole.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_idfield + warning that the flow isn't wired yet. - Accent: Google brand yellow
#fbbc04.
Microsoft OneDrive — scaffold (OAuth pending)¶
transport/onedrive.rs— same shape asgdrive.rs. Connect succeeds, list / transfer error with hint toportal.azure.comAAD app registration.- Site Form extras:
client_id+tenantfields. - Accent: Microsoft blue
#0078d4.
Plumbing changes¶
bridge::normalize_protocol()now acceptsdropbox(aliasdbx),gdrive(aliasesgoogle-drive,googledrive), andonedrive(aliasesone-drive,msgraph).post_connectdispatches the three new protocols to their respectiveTransport::connectconstructors.- Frontend
PROTO_ACCENTSmap gets three new entries with proper colors / kicker text / mark badges (DBX/GDR/1DR). SINGLE_PANE_PROTOSset 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 quirktransport::dropbox::tests::missing_token_yields_helpful_errortransport::gdrive::tests::pending_error_carries_setup_hinttransport::onedrive::tests::pending_error_carries_setup_hintbridge::tests::normalize_protocol_cloud_aliasesNATIVE_MENU_IDScontract 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/sitesPOST. 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…/Disconnectbuttons 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 broken —
openCtxMenuandopenMenuwere setting inlineleft/topon the inner.ctx-menu/.dropdownelements, but only the outer#ctx-root/#dropdown-roothasposition: 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 opensgogreenpaperlessinitiative.comin the user's browser. .brand-lockup/.brand-primary/.brand-secondaryCSS preserved so the "GoGreen Transit" text still reads alongside the logo.
Logging¶
statusLogentries 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.
Navigation¶
- 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 acmdwire 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-logsin 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.rsinstalls a real Windows menubar via Tauri'sMenuBuilder/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-actionviawindow.eval(__ftpMenuAction(id))which routes to the existing JS dispatchers — no@tauri-apps/apinpm dependency required. NATIVE_MENU_IDSconst + 3 unit tests assert the id contract between Rust and JS never drifts silently.
Layout-per-protocol¶
.workspace.single-paneCSS 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↑ Uploadaction 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-bodyusesheight: 200pxandmax-height: 200px+overflow-y: auto(wasmax-heightalone, which let the box shrink to content height instead of holding 200px)..workspaceuses fixedheight: 520pxso the panes are bounded and.rowsactually scrolls inside them (previously unboundedmin-height: 460pxgrew the pane to fit 50+ files).applyTheme()is dark-locked — config.theme no longer leaks atheme-lightclass that was washing out colours.--accent-sftpcorrected from#5eead4to#7dffb2(matches mock)..action.disabledis nowcolor: var(--ink-mute)only (noopacity: 0.5) — disabled buttons are still legible.- Session-tab strip moved out of
.shell-headinto 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/.failclasses at 11px mono (was.queue-summaryat 10px uppercase); separators are mid-dot·atvar(--ink-faint); "done" rendered in--ok, "failed" in--err— matches mock verbatim. - Statusbar uses inline
.okgreen dot +.vvalues with units baked in ("0 KB/s","— ms"); dropped the.uunit span that was dimming units withvar(--ink-faint). - Body padding
36px 36px 64px, max-width1440pxon concept-tag / tabs-strip / shell / palette / footnote — matches mock exactly. - Added the mock's
rise/rowIn/wireInkeyframe animations. - Wire-line rows stamped with
wire-newclass so new entries animate in, matching the mock's streaming effect.
In-app connect paths¶
Connect…·Sites…·Disconnectactions 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-finalshown 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¶
renderFileListnow emits<div class="row">with the mock's structure: glyph (↑ / ▸ / ◆ / ⇢), name (.n+.extpill + 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).renderSessionTabsnow emits.tabwith a coloured.chip.<proto>square, a.xclose button, and a trailing dashed.tab-add.renderTransfersemits.q-itempills (active / ok / fail) with direction arrow, filename, and status/percent chip.statusLogwrites.wire-line.tx/.rx/.sys/.err/.warnrows with[ts] [TAG] [msg]shape; rx pulls the numeric response code into a.codespan, 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 withpulse · live · host · uptimewhen connected; a rolling tick updates uptime every second.updateModeRail()populateschannel / type / AUTH TLSpills from the live session.updateWireMeta()keepsCTRL :<port>andTLS · session · N framescurrent.
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 — existingMENUSdispatch unchanged. - Connect action opens a focused Quick-Connect modal that writes back
to the hidden
#qc-*inputs then callsquickConnect(). - Keyboard shortcuts:
Uupload,Ddownload,Rrefresh,F5refresh both,Escclose dropdowns/menus (existing).
Chrome¶
- Drag-over highlight rule
.pane.drag-over+ pseudo "drop to transfer" label added todist/styles.css. - Hidden
#local-path/#remote-pathtext inputs retained for backwards compat with existingnavigateLocal/Remoteplumbing; 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
ftproxyhandle for continuity.productNameand window title intauri.conf.jsonupdated.
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
--accentCSS variable drives the whole UI;applyAccent()indist/app.jsre-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.jspreserved 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¶
ConnectionInfoand the bridgeConnectRequestgained anextra: HashMap<String, String>field.SavedSite.extra(optional, serde-default so existing sites still load) persists protocol-specific settings alongside host/user.post_connectmergesextrafrom the connect body with any stored on the referencedsiteIdso the UI doesn't have to resend cloud config every time.
S3 transport (transport::s3)¶
- Built on
aws-sdk-s3(v1). Probes withHeadBucketon connect. - Inputs:
username= access key,password= secret access key,extra.bucket(required),extra.region(defaults tous-east-1),extra.endpoint(forces path-style, enabling MinIO, Cloudflare R2, DO Spaces, Wasabi, etc.),extra.path_styleoverride. - Operations:
list/list_pageviaListObjectsV2with/delimiter (common prefixes become directories);mkdircreates a zero-byteprefix/marker;rmdirbatchesDeleteObjectsin chunks of 1000;rename= CopyObject + DeleteObject;download/upload+ streamingdownload_to/upload_from;sizeviaHeadObject. - Single-part
PutObjecttoday; multipart (required for >5 GB) is a follow-up.
Azure Blob transport (transport::azure)¶
- Built on
azure_storage+azure_storage_blobs. Probes withget_propertieson the container. - Inputs:
username= account name,password= account key,extra.container(required). - All operations target BlockBlobs.
rename= CopyBlob (server-side) - DeleteBlob.
list_pagecurrently returns one page per call; Azure's continuation plumbs through the same stream.
GCS transport (transport::gcs)¶
- Raw REST against
storage.googleapis.comwith a service-account JWT exchanged for an OAuth2 access token (cached, refreshed ≥30 s before expiry). Avoids the fullgoogle-cloud-storagecrate to keep the dep tree tight — only addsjsonwebtoken+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_pageusesobjects.list?delimiter=/;uploadusesuploadType=media;renameusesrewriteTo;rmdiriterates + deletes (no batch delete on GCS).
Bridge / routing¶
normalize_protocolacceptss3,azure/azureblob/azure-blob,gcs/gs, plus the existing ftp / sftp / webdav.post_connectdispatches to the right transport constructor.port = 0is 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.
connectSavednow sends the site's storedextrain the connect body so cloud tabs reconnect cleanly.
Client crate¶
opensentinel-ftproxy-clientgainsConnectRequest.extra,SavedSite.extra, andNewSite.extrafields. Consumers targeting cloud backends pass the same key/value pairs the bridge expects.
Tests¶
cargo test --libmain: 74/74 passing (was 69).cargo testclient 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 onConnectionInfo.
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 throughopts.continuation. Low-priority until someone hits a1000-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_slotsis nowRwLock<Arc<Semaphore>>.PUT /configwith a newconcurrencyvalue 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/uploadand/downloadroute through a newslot_for_session/lock_for_sessionhelper 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_downloadwere extracted intorun_transfer_upload/run_transfer_downloadfunctions that take an optionalsession_id; the old routes delegate withNone.
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.rsnow exposesbuild_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— runscargo test --libon the main crate pluscargo test --all-featureson the client crate.endpoint-tests— spinsscripts/docker-compose.test.yml(atmoz/sftp + pure-ftpd), builds + runsftproxy-bridge, locates the token in the Linux XDG data dir, and executesscripts/test-endpoints.ps1withFTPROXY_TEST_STACK=docker.test-endpoints.ps1token lookup is cross-platform:FTPROXY_TOKENenv var wins, thenAPPDATA,XDG_DATA_HOME,HOME/.local/share, with a final recursive fallback.
WebDAV transport (first real plugin)¶
- New
transport::webdavmodule implementing theTransporttrait viareqwest+quick-xml. Covers PROPFIND (list+list_page), MKCOL, PUT (buffer-first upload with cancel), GET (streamingdownload_towith 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) andpost_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 --libmain crate: 69/69 passing (was 64).cargo testclient 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
SessionSlottype holding its owninfo: RwLock<SessionInfo>andtransport: Arc<Mutex<Option<Box<dyn Transport>>>>.AppStatenow ownssessions: 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.changedWS 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¶
RemoteEntrynow documents akind = "object"value for S3-like backends;ListPage { entries, next_token }+ListOpts { prefix, continuation, limit }added to theTransporttrait with a default impl that returns the whole listing (preserving SFTP/FTP behaviour).- New
GET /files/remote/page?path=&continuation=&limit=endpoint. transport::pluginsubmodule 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 = trueon 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 reportunverified. - Host-key mismatch modal: the bridge now emits
hostkey.mismatchwith{ 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 callsPOST /hostkeys/trust. test-endpoints.ps1docker switch: set$env:FTPROXY_TEST_STACK = "docker"to point the suite atscripts/docker-compose.test.yml(atmoz/sftp on 22322, demo/demo). The script auto-provisions a temporary site in that mode.
Tests¶
cargo test --libon the main crate: 64/64 passing (was 60).cargo testonopensentinel-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.jsonwith fields:defaultLocalPath,concurrency,logLevel,theme,verifyAfterUpload,ftpTransferType,completionSound,strictHostKey. Loaded at startup, clamped/validated on write.bookmarks.rs—Bookmarkas(site_id?, remote_path, local_path?, note?). Persisted tobookmarks.json. Distinct fromSavedSiteso one site can have many working directories.known_hosts.rs— SFTP fingerprint store (JSON, not OpenSSH format) withcheck()returningUnknown | 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 transferbytesTotal(SFTPmetadata, FTPSIZE).remote_hash(remote, algo)— FTP issuesXMD5/XSHA256/XCRCvia the SITE-like channel; SFTP returnsNone.ProgressFncallback 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_policyrecords theSeenKey(algorithm + fingerprint + verdict) from russh'scheck_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.seenWS 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: truein the connect body.
New endpoints¶
GET /config— server-side settings.PUT /config— validate + persist + broadcastconfig.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 FTPHASH/XMD5/XSHA256/XCRCresponse.
Transfer pipeline¶
transfer_download/transfer_uploadnow call the streaming variants and emittransfer.progressWS events throttled to 250 ms.- Cooperative cancel:
DELETE /transfers/:idsignals the worker via awatch::Sender<bool>stored inAppState.cancel_flags. The worker short-circuits between chunks, surfaces atransfer.cancelledevent, and cleans up the partial file for downloads. transfer_slots: Arc<Semaphore>gates concurrent transfers toAppConfig.concurrency(default 2, clamp 1–16).- Optional post-upload integrity verify: when
config.verifyAfterUploadis on, the worker computes local MD5/SHA-256/CRC32, asks the server for its own hash, and attachesverified: true|false|nullto the response.
Frontend¶
- Dark mode with a new
theme-darkbody class and a three-way Settings toggle (light / dark / auto). "Auto" followsprefers-color-scheme. - Settings modal now reads/writes
/configon 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.progressevents. Per-row cancel button (✕) issuesDELETE /transfers/:id. - Directory sync modal (View → Compare directories) uses
POST /dir/compareand 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.ymlbrings upatmoz/sftpon 22322 andstilliard/pure-ftpdon 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
AppStaterefactor (would invalidate the/session/*contract for external consumers). Tracked intodo.md. - Plugin surface for S3/WebDAV/GCS —
RemoteEntrygeneralization. - Standalone
opensentinel-ftproxy-clientcrate (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
TcpListenerHTTP toy with anaxum+tokioserver. 29 routes, bearer-token middleware, CORS, WS. Transporttrait with two production impls:- SFTP via
russh0.45 +russh-sftp2.1 (pure-Rust, async). Upload usesopen_with_flags(CREATE | WRITE | TRUNCATE)(OpenSSH's sftp-server rejects the flagscreate()emits). - FTP via
suppaftp6 withasync+async-rustlsfeatures. Usesretr_as_stream/finalize_retr_streamandput_filewithfutures_util::io::Cursor. - Localhost bind, preferred port 7878 with fallback to an OS-assigned
ephemeral port (
FTPROXY_PORTenv override). - Bearer-token auth on every endpoint except
/health. Token generated on first launch, written to the app data dir. bridge.urldiscovery 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 3with thewindows-native(+apple-native,sync-secret-service) feature flags explicitly enabled. - WebSocket
/eventswith 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.jsonsetsdragDropEnabled: falseso the webview handles HTML5 drag-drop natively (Tauri's native interceptor blocks drop events otherwise).- Loopback bind only. Bearer token required on all non-
/healthendpoints. Passwords never persist to disk in cleartext. .gitignorecoverssrc-tauri/target,src-tauri/gen/schemas, IDE junk,tasks/.
Infrastructure¶
- Provisioned an SFTP account on the IONOS VPS:
- User
gogreensuites_ftpchrooted to/srv/sftp/gogreensuites. /var/www/gogreensuites.combind-mounted into the jail via/etc/fstab.- Docroot set to
root:deploy 2775so 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 withcargo test --lib. Handler-level assertions usetower::ServiceExt::oneshotso 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 checkclean (3 benign dead-code warnings).- Upload integrity spot-checked: local
md5summatches remotemd5sumbit-for-bit.
Observability¶
tracing_subscriber+tower_http::TraceLayerwired. Every HTTP request now emits a span with method, URI, status, and duration.#[tracing::instrument]onpost_connect,transfer_upload,transfer_downloadwith host/port/user/path fields.RUST_LOG=debugopens 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
logWebSocket event; last 1000 entries also available viaGET /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).