Appearance
Build the Desktop Tray App
services/desktop (@tt/desktop) is a small Tauri app for macOS and Windows: an icon in the menu bar / system tray that opens a popup where a worker adds a time entry.
It is not a second frontend. The popup is a webview pointed at the deployed web client's /tray route (services/client/src/views/TrayQuickAdd.vue), so the form, validation, project and task pickers, offline queue and sign-in are exactly the ones the web app ships, and the session cookie is a first-party cookie on the API's own origin — no bearer tokens, no CORS changes. The Rust shell only owns what a browser tab cannot: the tray icon, positioning the popup next to it, hiding it on focus loss, and telling the page when it has just been shown.
Where things live
| Path | Contents |
|---|---|
services/desktop/src-tauri/src/lib.rs | The whole shell: tray icon + menu, popup window, show/hide/toggle, URL guard |
services/desktop/src-tauri/tauri.conf.json | Product name, identifier, bundle targets, icons |
services/desktop/src-tauri/capabilities/default.json | What the remote page may call over IPC, and from which known origins (the configured server's origin is added at startup) |
services/desktop/src-tauri/icons/ | Bundle icons (generated) and the tray icons |
services/desktop/ui/index.html | Placeholder frontendDist; never shown |
services/client/src/views/TrayQuickAdd.vue | The page the popup loads |
services/client/src/components/EntryQuickForm.vue | The quick-add form, shared with the "Nouvelle saisie" sheet |
services/client/src/lib/desktop.ts | The page's side of the bridge: hide the popup, listen for "shown" |
.github/workflows/desktop-build.yml | Manual workflow that produces the .dmg and Windows installer |
Prerequisites
- The repo's usual toolchain (Node 22, pnpm) —
pnpm installinstalls the Tauri CLI. - A stable Rust toolchain: https://rustup.rs.
- Tauri's platform dependencies, see https://tauri.app/start/prerequisites/:
- macOS: Xcode Command Line Tools.
- Windows: Microsoft C++ Build Tools and the WebView2 runtime (already on Windows 11).
- Linux / WSL (for
cargo checkand dev runs only — there is no Linux release):libwebkit2gtk-4.1-dev,libgtk-3-dev,libayatana-appindicator3-dev,librsvg2-dev.
Run it against your local client
The popup loads a URL, so point it at the Vite dev server and run the shell:
bash
pnpm dev:client # client on http://127.0.0.1:35173
TIM_DESKTOP_URL=http://127.0.0.1:35173 pnpm --filter @tt/desktop tauri:devClick the tray icon: the popup opens on /tray, and the client's /login page if you are signed out. Hot reload works as usual because it is the client's own dev server.
tauri dev compiles the Rust side on first run (a few minutes); later runs are incremental.
Compile-check without running
bash
pnpm --filter @tt/desktop cargo:checkThis is the Rust equivalent of pnpm typecheck and works on Linux/WSL too. It is not part of the CI pipeline (the runners have no webkit), so run it before opening a PR that touches services/desktop.
Build installers
Locally, on the platform you want an installer for:
bash
pnpm --filter @tt/desktop tauri:buildOutput lands in services/desktop/src-tauri/target/release/bundle/ — dmg/ and macos/ on macOS, nsis/ on Windows.
You cannot build a macOS app from Windows or Linux, which is what the Desktop app (manual) workflow is for: trigger it from the Actions tab, pick the server URL, and download the artifacts from the run. It is workflow_dispatch only — macOS runners bill at 10× the Linux rate — so it never runs on push or PR.
Builds are unsigned. On macOS the first launch is right-click → Open; on Windows, SmartScreen's "More info → Run anyway". Signing and notarization need an Apple Developer account and a code-signing certificate; wire them into the workflow (APPLE_CERTIFICATE, APPLE_ID, … per the Tauri docs) when there is one.
Point it at another server
The URL the popup loads is TIM_DESKTOP_URL + /tray, resolved in this order:
TIM_DESKTOP_URLin the environment of the running app (handy for testing a build against staging without rebuilding);TIM_DESKTOP_URLat build time, baked into the binary (what the workflow input sets);https://tim.ovh.
Two things must agree with that URL:
- The page may only call the shell from an origin a capability grants.
capabilities/default.jsoncoverstim.ovh,*.tim.ovh(organization subdomains) and the local dev server, checked at build time; at startup the shell grants the same permissions to the origin of whateverTIM_DESKTOP_URLresolved to (grant_server_ipcinlib.rs), so a self-hosted server needs no edit. A page served from an origin neither covers — one the server redirects to, say — still loads, but its calls to hide the popup are refused: it then relies on the shell's hide-on-blur, and the "saved" confirmation lingers until the next click. Add such an origin to the file. - The client route must exist on that server (
/trayshipped with this feature), which just means the client must be at least as new as the desktop app.
How the page and the shell talk
withGlobalTauri is on, so the shell injects window.__TAURI__ into the page — the client needs no @tauri-apps/api dependency. services/client/src/lib/desktop.ts wraps the two calls the page makes:
hideDesktopPopup()after a save (and on Escape / the close button) —core:window:allow-hide;onDesktopPopupShown(handler)to reset the form every time the shell shows the popup again —core:event:allow-listen. The webview lives for days; without this the entry's default day would be the day the app was launched.
Everything degrades to a no-op in a normal browser tab, so /tray is also just a page.
The shell, for its part, checks the webview's URL each time it shows the popup. If the page has wandered off /tray (an expired session that signed back in and landed on the app home) and is not on one of the sign-in pages, it navigates back.
Icons
pnpm --filter @tt/desktop tauri:icon regenerates the bundle icon set from services/client/public/logo_hq.png; delete the android/ and ios/ folders it also produces. The tray icon (tray-44.png, scaled to 18pt by macOS) is rasterized from services/client/public/icons/safari-pinned-tab.svg — a monochrome mask, which is what macOS wants for a template icon that follows the menu bar's light/dark tint. Windows and Linux show the colour 32x32.png instead.