§ 01.01 / SPEC  ·  TRAK  ·  仕様
0—TRAK—2026—01
← INDEX

TRAK

SPEC-01.01  ·  MACOS · MAS-LIVE

Calendarized time tracking for the macOS menu bar. Shipped on the Mac App Store.

trak landing page hero — menu bar simplicity, Mac App Store badge

Start a named timer in the menu bar; when you stop it, the elapsed block lands on the Google Calendar you assigned to it. The product is small. The interesting parts are the Electron-to-App-Store path, the multi-window state sync, and bundling an MCP extension so Claude can drive timers in natural language.

Stack

Electron + Vite
Two renderer windows (main + settings) with HMR on both; main process owns services and the menu bar tray.
React + Zustand
Single store; windows subscribe via a thin IPC bridge so state diverges by zero.
TypeScript end-to-end
Shared types between main/preload/renderer; preload exposes a typed window.api instead of stringly-typed ipcRenderer.invoke.
Service container
Tiny DI for TimerService, StorageService, GoogleCalendarService — easy to swap real Google client for a mock in tests.
Google Calendar API
OAuth on first run; events written to the per-timer calendar on stop.
MCP .dxt extension
A double-click install for Claude Desktop — no editing claude_desktop_config.json.
Vitest + Playwright
Service unit tests + cross-window integration tests.

Process

The first version was a single-window React app talking to Google Calendar through ad-hoc IPC handlers. It worked for one timer. Adding the settings window broke state in three places, so I rewrote it around services and dependency injection — services are interfaces, the container wires concrete implementations, and tests substitute mocks at registration time. The renderer keeps no logic worth testing; the main process holds all of it.

trak main menu bar UI with multiple timers, start buttons, and quick-add field
Main menu bar window. Each timer maps to a calendar.

Multi-window state sync is the part that surprises you in Electron — you can't just share a Zustand store across processes. The pattern that worked: store lives in the renderer, but every mutation goes through an action handler in the main process, which then broadcasts the new slice back to every window. The settings window opens and immediately has the same timer list the main window had a millisecond ago.

trak settings window — add timers, choose calendar, visibility toggles
Settings: per-timer calendar assignment, visibility, Claude Desktop install.

The Mac App Store path was the biggest time sink — sandboxing, entitlements, and notarization for App Store differ from notarization for Developer ID, and the failure messages from altool aren't kind. The repo carries a custom scripts/notarize.js that drops out of electron-builder's default flow for the MAS branch.

Claude Desktop integration ships as a generated .dxt file — the user installs it like any extension and Claude can start/stop timers by name. The MCP server runs in-process when trak is launched; no separate daemon to manage.