Files
stream_deck_notion_timer/CLAUDE.md
2026-04-10 19:51:21 +01:00

3.2 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Build & Deploy

# 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.tsbin/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.