# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Build & Deploy ```bash # Build (TypeScript → bundled JS) npm run build # Watch mode during development npm run dev # Deploy to Stream Deck after building cp com.pdma.notion-timer.sdPlugin/bin/plugin.js "$HOME/Library/Application Support/com.elgato.StreamDeck/Plugins/com.pdma.notion-timer.sdPlugin/bin/plugin.js" pkill -f "notion-timer" # Stream Deck restarts the process automatically # Full install (first time — copy plugin folder, not symlink; Stream Deck doesn't follow symlinks) cp -r com.pdma.notion-timer.sdPlugin "$HOME/Library/Application Support/com.elgato.StreamDeck/Plugins/" ``` There is no test suite. Manual testing is done by pressing buttons in the Stream Deck app and checking the Notion database. ## Architecture The plugin has two distinct runtime contexts that communicate via WebSocket (managed by the SDK): **Plugin process** (`src/plugin.ts` → `bin/plugin.js`) runs in Node.js 20 under Stream Deck: - One `TimerToggle` class (extends `SingletonAction`) handles all button instances of the action - `settingsCache` (Map keyed by `action.id`) is required because the SDK doesn't expose other actions' settings — it must be updated after every `setSettings()` call, not just on appear - Single-timer enforcement: when starting a timer, iterate `this.actions` and stop any other active timer via `settingsCache` - Global settings (token, DB IDs, user ID) are stored as hardcoded `DEFAULTS` in `plugin.ts` **Property Inspector** (`ui/property-inspector.html`) runs in a browser frame inside the Stream Deck app: - Uses Elgato's official `$PI` library (files in `ui/libs/`) — **do not modify these files** - The libs were copied from an installed Time Tracker plugin; they are not in npm - Settings are saved via `$PI.setSettings()`, not a custom WebSocket implementation - Event listeners must be attached inside `$PI.onConnected()`, not `DOMContentLoaded` - Projects are sent from the plugin to the PI via `streamDeck.ui.sendToPropertyInspector()` when `onPropertyInspectorDidAppear` fires (plugin pushes, PI doesn't pull) **Critical SDK v2 patterns** (different from v1 examples online): - Use `streamDeck.ui.sendToPropertyInspector()` not `ev.action.sendToPropertyInspector()` - Use `streamDeck.ui.onSendToPlugin()` not `SingletonAction.onSendToPlugin` (the method never fires in v2) - `SingletonAction.onDidReceiveSettings` also does not reliably fire — settings come via `$PI.onDidReceiveSettings` on the PI side **Notion API** (`src/notion.ts`): - Notion pages have icons of `type: "emoji"` (direct emoji string) or `type: "icon"` (named SVG icon with `name` + `color` fields) - Named icons are mapped to emoji via lookup tables: `book`+color → colored book emoji, `skip-forward`+color → ⏭+circle, others in `ICON_NAME` - Timer entries: created with `Status: "Running"`, patched with `Status: "Stopped"` and `End Time` ## Debug Stream Deck logs are at `~/Library/Logs/ElgatoStreamDeck/`. The plugin process stderr also appears there. The `streamDeck.ui.onSendToPlugin` handler in `plugin.ts` still writes to `/tmp/sd-debug.log` — this should be removed once debugging is no longer needed.