Skip to main contentGBOSSTABTime and Billing

Working offline

Desktop appHow the desktop buffers entries when you lose connection.

Working offline

Coming soon: offline buffering. The desktop is scaffolded for offline-first time tracking, but the wiring isn't done, today every Start and Stop hits the API directly, and the desktop is effectively online-only.

This article describes what's actually in the app today vs. what's planned, so you know what to expect.

What's there today

When the desktop starts, it initialises a small SQLite database at ~/Library/Application Support/gboss-tab/tab-buffer.sqlite with a single pending_ops table. The schema and helper functions exist in storage.rs, enqueue, pending, drop_op, record_failure, all marked #[allow(dead_code)] // wired up from the React UI in a future polish pass.

A background thread in sync.rs fires every 30 seconds and emits a tab://sync-tick event into the React layer. The event currently has no listener that flushes anything, it's a heartbeat with no payload.

The only buffer-related command the UI actually invokes is cmd_buffer_count, which always returns 0 because nothing enqueues.

In practice that means:

  • Click Start and the request goes straight to /api/v1/timer/start. If the network is down, you get an error in the alert strip and the timer doesn't start.
  • Click Stop and the request goes straight to /api/v1/timer/{id}/stop. Same, needs the network to be up.
  • The 30-second sync tick exists but doesn't flush anything, because no entries are queued to flush.
  • The "Recent entries" list at the bottom of the desktop is pulled from the backend on each reload, not from a local cache. If the backend can't be reached, the list stays empty (or stale from the last successful load).

What's planned

Once the wiring lands (it's a Phase 1E polish item per the comments in storage.rs), the desktop will behave like this:

  • Every Start / Stop writes a pending_ops row first, with a client-generated UUID.
  • The 30-second sync tick drains the queue to /v1/timer/sync, which is already idempotent against the client-generated UUID (there's a partial unique index on (user_id, client_op_id) in the schema).
  • The backend dedupes by client_op_id, so retries on a flaky connection don't double up.
  • A status strip will show "Synced a few seconds ago" / "Synced 14 minutes ago" so the user can tell when they've gone offline.

The DTO for sync ops (SyncOp) and the resource endpoint (POST /v1/timer/sync) already exist in the backend. The remaining work is the React side: enqueueing on Start/Stop, listening to tab://sync-tick, draining, handling failures.

In the meantime

Two practical implications today:

  1. The desktop is an online-only client. Network drops break starting and stopping timers, just like in the web app.
  2. Don't trust the "buffer" terminology if you see it in source / Rust logs, it's there for the future feature, not in use today.

If you regularly work in places with bad connectivity (hotel Wi-Fi, trains, client sites with locked-down networks), expect the same intermittent behaviour you'd get from the web app: requests fail until connectivity returns.

Why entries won't double up (when offline lands)

The dedup story is the most thought-through part of the design, so it's worth knowing: every queued op carries a UUID generated on the device, and the server's /v1/timer/sync resource has a partial unique index that rejects a second op with the same (user_id, client_op_id). Retries during a flaky connection will be ignored after the first acceptance.

That guarantee is real today at the API layer, it just isn't being exercised, because nothing enqueues.

Related