Activity Timer is a sophisticated Trello Power-Up that solves time tracking and estimation challenges within Trello boards. It enables team members to track time spent on cards, set estimates, receive notifications when approaching time limits, and export comprehensive reports.
Traditional time tracking requires context-switching to external tools, leading to:
- Forgotten time entries
- Inaccurate time logs
- Difficulty correlating work with specific tasks
- No visibility into time vs. estimates
- Complex data export processes
Activity Timer integrates time tracking directly into Trello's UI, making it seamless and context-aware.
- Framework: Vue 3 (Composition API)
- Build Tool: Vite
- UI Library: PrimeVue (DataTable, Dropdown, DatePicker, etc.)
- Styling: SCSS + PrimeFlex (utility CSS)
- TypeScript: Full type safety throughout
- Error Tracking: Sentry for production error monitoring
- Integration: Trello Power-Up Client API
- API Gateway HTTP: Webhook receiver for Trello events
- API Gateway WebSocket: Real-time communication for auto-timer feature
- Lambda Functions:
- HTTP handler for webhook processing
- WebSocket handler for connection management
- DynamoDB: Stores WebSocket connection mappings (connection_id ↔ member_id)
- CloudFront + S3: Static asset delivery
- Infrastructure as Code: AWS CDK (TypeScript)
- Linting: ESLint + Prettier
- Type Checking: vue-tsc
- Local Dev: Vite dev server with HTTPS (mkcert)
Activity Timer stores ALL card data directly in Trello's native storage using the Trello Power-Up API. This is a key architectural decision that provides:
- No external database for card data
- Data persists with the card forever
- Accessible via Trello's REST API
- Automatic sync across all clients
All data is stored in shared scope (visible to all board members) using compressed array formats to minimize storage usage.
Purpose: Completed time tracking sessions
Structure: Array of arrays [memberId, startTimestamp, endTimestamp]
Location: Card-level, shared scope
Example:
[
["5f1a2b3c4d5e6f7a8b9c0d1e", 1633024800, 1633028400], // 1 hour session
["5f1a2b3c4d5e6f7a8b9c0d1e", 1633032000, 1633035600] // Another session
]Purpose: Currently running timers
Structure: Array of arrays [memberId, listId, startTimestamp]
Location: Card-level, shared scope
Note: Only one timer per member per board (automatically stops other timers)
Example:
[
["5f1a2b3c4d5e6f7a8b9c0d1e", "5f9a8b7c6d5e4f3a2b1c0d9e", 1633040000]
]Purpose: Time estimates per member
Structure: Array of arrays [memberId, timeInSeconds]
Location: Card-level, shared scope
Example:
[
["5f1a2b3c4d5e6f7a8b9c0d1e", 7200], // 2 hours
["6a2b3c4d5e6f7a8b9c0d1e2f", 3600] // 1 hour
]Purpose: Enable/disable auto-timer feature
Structure: 0 or 1
Location: Board-level, shared scope
Purpose: Which list triggers auto-timer start Structure: Trello list ID string Location: Board-level, shared scope
Purpose: Per-member notification thresholds, UI visibility Location: Member-level, private scope Note: Not synced across members
Reading Data:
const ranges = await t.get(cardId, 'shared', 'act-timer-ranges', []);Writing Data:
await t.set(cardId, 'shared', 'act-timer-ranges', serializedRanges);Storage Limits: Trello enforces a 4KB limit per storage key. Activity Timer compresses data by:
- Using minimal array structures (no property names)
- Unix timestamps instead of ISO strings
- Member IDs only (names fetched from Trello API when needed)
Central orchestrator for all card-related operations.
Responsibilities:
- Fetch/calculate time spent (running timers + completed ranges)
- Start/stop time tracking
- Automatically stop timers on other cards when starting a new one
- Handle threshold validation (prevents saving very short trackings)
- Error handling for storage limit exceeded
Key Methods:
getRanges(): Fetch completed time rangesgetTimers(): Fetch active timersgetEstimates(): Fetch estimatesstartTracking(listId): Start timer (stops all other timers first)stopTracking(): Stop timer and save to rangesgetTimeSpent(memberId?): Calculate total time
Collection class for time tracking sessions.
Methods:
timeSpent: Sum of all range durationsadd(range): Add new rangeserialize(): Convert to storage formatsave(): Persist to Trellofilter(fn): Filter ranges by predicate
Collection class for active timers.
Methods:
getByMemberId(id): Find specific member's timerstartByMember(memberId, listId): Start timer for memberremoveByMemberId(id): Remove timertimeSpent: Current running time for all timers
Collection class for time estimates.
Methods:
getByMemberId(id): Get member's estimatetotalEstimate: Sum of all estimatesremoveByMemberId(id): Remove estimate
Key Functions:
setTrelloInstance(t): Store Trello client instancegetTrelloInstance(): Retrieve client for API callsgetMemberId(): Get current member's Trello ID (cached)getValidToken(): Get REST API token (validates not error token)isAuthorized(): Check if user authorized REST API accessclearToken(): Clear token & associated webhooksprepareWriteAuth(): Clear read-only tokens before write operations
Authorization Pattern: Activity Timer uses two authorization modes:
- No Auth: Basic Power-Up features (stored in Trello's plugin data)
- REST API Auth: Required for:
- Auto-timer webhooks
- Data export features
- Calendar view
Users can grant read-only OR read-write access. The app gracefully handles revoking read-only tokens when write access is needed.
Purpose: Real-time communication for auto-timer feature.
Flow:
- User enables auto-timer and selects a list
- Frontend establishes WebSocket connection
- Sends
{ listen_member_id: "<memberId>" }on connect - Backend stores connection in DynamoDB
- When user moves card to configured list:
- Trello webhook fires
- HTTP Lambda validates board settings
- Queries DynamoDB for member's WebSocket connection
- Sends
{ type: 'startTimer', cardId: '...' }message - Frontend auto-starts timer
Connection Lifecycle:
- Auto-reconnects on close (API Gateway 10-min timeout)
- Rate limited (5-second cooldown between reconnects)
- Connection removed from DynamoDB on disconnect
Activity Timer implements all major Power-Up extension points:
Displays on card fronts in board view
Badges shown:
- Clock icon with total time spent
- Calendar icon with estimate (per member or total)
- Visual indicators for time vs estimate
Buttons at top of card back
Four buttons:
- Start/Stop Timer: Primary time tracking action
- Manage Time: Open time editor modal
- Notifications: Configure threshold alerts
- Settings: Member-specific settings
- Time Spent: View detailed breakdown by member
Implementation Pattern:
callback: async (t) => {
return t.popup({
title: 'Popup Title',
url: './index.html?page=page-name',
height: 500
});
}Persistent section at bottom of card
Shows:
- Current timer status
- Quick actions (Start/Stop, Edit Time, Change Estimate)
- Total time spent summary
- Estimate display
Components:
view.vue: Main display componentadd_time_manually.vue: Manual time entry modalchange_estimate.vue: Estimate editor
Buttons in board menu
Three buttons:
- Enable Notifications: Request browser notification permission
- Calendar View: Week calendar with all trackings
- Settings: Auto-timer, data export, member preferences
Premium Features (requires subscription):
- Data export (Time Tracking & Estimates to CSV)
- Advanced filtering
- Calendar view
Settings menu integration
Displays:
- Current settings
- Authorization status
- Premium feature access
Purpose: Webhook receiver for Trello card movement events.
Security: Validates webhooks using HMAC-SHA1 signature:
const signature = crypto.createHmac('sha1', TRELLO_SECRET)
.update(body + callbackURL)
.digest('base64');Event Flow:
- Receive POST from Trello webhook
- Verify signature (return 410 to de-register if invalid)
- Check if action is
updateCardwith list change - Fetch board's plugin data via Trello API
- Validate auto-timer is enabled and list matches
- Query DynamoDB for member's WebSocket connection
- Send message to connection via API Gateway Management API
- Handle dead connections (remove from DynamoDB)
Error Handling:
- Returns 410 to de-register webhook on persistent errors
- Cleans up stale WebSocket connections
- Validates plugin data to prevent unnecessary processing
Routes:
$connect: Accept connection$default: Handle incoming messages (stores member_id mapping)$disconnect: Clean up DynamoDB entry
DynamoDB Schema:
connection_id (Partition Key) | member_id | api_id | region | stage
GSI: MemberIdIndex on member_id
CDK Stack Creates:
- S3 Bucket: Static assets
- CloudFront Distribution:
- CDN for global delivery
- Security headers injection (CSP, HSTS, X-Content-Type-Options)
- Custom viewer response function
- HTTP API: Webhook endpoint
- WebSocket API: Real-time communication
- DynamoDB Table: Connection mappings with GSI
- Lambda Functions: HTTP & WebSocket handlers
- IAM Roles: Least-privilege access
Security Headers (Required by Trello):
Content-Security-Policy: Restricts resource loadingStrict-Transport-Security: Enforces HTTPSX-Content-Type-Options: Prevents MIME sniffing
Route Determination:
- Check URL query param:
?page=<name> - Check Trello iframe args:
t.args[0].page
Available Pages:
card-back-section: Main card displaychange-estimate: Estimate editoradd-time-manually: Manual time entrymember-settings: Personal settingsnotification-settings: Alert thresholdssettings: Board-level settingscalendar: Week calendar view (premium)time: Time tracking data exporter (premium)estimates: Estimates data exporter (premium)datetime: Date/time picker componentenable-notifications: Trigger browser permission request
Theme Support: Dynamically loads PrimeVue light/dark theme based on Trello's theme.
Challenge: Trello's 4KB limit per storage key Solution:
- Minimal array structures (no JSON objects with keys)
- Timestamps as Unix integers
- Only store IDs, fetch names on-demand
- Alert users when limit approached
Challenge: Only one timer should run per member Solution: When starting timer, iterate all cards and stop any running timers
Challenge: localStorage unavailable in incognito Solution:
let incognito = false;
try {
window.localStorage.getItem('incognito-test');
} catch (e) {
incognito = true;
}
// Initialize Trello without auth if incognitoChallenge: Users can grant read-only access, but webhooks need write
Solution: prepareWriteAuth() detects read-only tokens and clears them before write operations
Challenge: Connections persist in DynamoDB after client disconnects unexpectedly Solution:
- Try to send message
- Catch API Gateway error
- Delete connection from DynamoDB
Challenge: Prevent unauthorized webhook calls Solution: Validate HMAC signature on every request, de-register webhook on failure
Challenge: Accidental clicks create 1-second trackings Solution: Threshold setting (default 5 seconds) prevents saving short sessions
Challenge: Sentry flooded with "PluginDisabled" errors
Solution: Filter these errors in beforeSend hook
Memory: Logs use console.debug() level so they're stripped in production. Only output locally or when debugging.
Memory: Avoid cache tags due to memory leaks.
All Trello API calls should be wrapped in try-catch. Card operations that can exceed storage limits should show user-friendly alerts:
try {
await ranges.save();
} catch (e) {
if ((e + '').includes('4096 characters exceeded')) {
t.alert({ message: 'Too many time trackings...', duration: 6 });
}
}Trello types in src/types/trello.d.ts provide full IntelliSense for Power-Up API.
- Start/Stop:
src/components/card.ts(startTracking,stopTracking) - Timer Display:
src/capabilities/card-buttons/callbacks/TimeSpent.ts - Manual Entry:
src/capabilities/card-back-section/add_time_manually.vue
- Model:
src/components/estimate.ts,src/components/estimates.ts - UI:
src/capabilities/card-back-section/change_estimate.vue - Badge Display:
src/capabilities/card-badges/index.ts
- Config:
src/pages/NotificationSettings.vue - Trigger:
src/utils/notifications.ts - Permission:
src/Router.vue(enable-notifications page)
- Settings:
src/utils/auto-timer.ts - WebSocket:
src/components/websocket.ts - Backend Webhook:
src/api/http/index.js - Backend WebSocket:
src/api/websocket/index.js
- Time Tracking:
src/pages/DataExporter/TimeTracking/index.vue - Estimates:
src/pages/DataExporter/Estimates/index.vue - Premium Check: Look for subscription validation
- Implementation:
src/pages/WeekCalendar/index.vue - Features: Week navigation, member filtering, visual timeline
- Board Settings:
src/pages/Settings.vue - Member Settings:
src/pages/MemberSettings.vue - Settings Model:
src/components/settings.ts - Local Storage:
src/utils/local-storage.ts
- Icon System:
src/components/UIIcon/ - Date Picker:
src/components/DatetimePicker.vue - Loader:
src/components/UILoader.vue
- CDK Stack:
infrastructure/lib/infrastructure-stack.ts - Deployment: GitHub Actions (
.github/workflows/) - Environment Vars:
.env(local), CDK env vars (production)
- Create directory in
src/capabilities/<capability-name>/ - Create
index.tswith capability function - Register in
src/main.tsinitialization - Add UI components if needed
- Create Vue component in
src/pages/ - Add route case in
src/Router.vue - Create callback in appropriate capability
- Update model in
src/components/(e.g.,range.ts) - Update collection class (e.g.,
ranges.ts) - Update serialization methods
- Consider backwards compatibility
- Modify Lambda in
src/api/http/orsrc/api/websocket/ - Test locally (requires AWS setup)
- Update CDK stack if infrastructure changes needed
- Deploy via
ACT_ENV=dev yarn cdk deploy
- Check browser console for storage errors
- Use Trello's API Explorer:
https://trello.com/power-ups/admin - Inspect card data:
GET /1/cards/{cardId}/pluginData - Check compressed size of serialized data
VITE_APP_NAME="Activity timer" # App name in auth dialogs
VITE_APP_KEY="<trello-api-key>" # From https://trello.com/app-key
VITE_WEBSOCKET="wss://..." # WebSocket API endpoint
VITE_API_HOST="..." # HTTP API host
VITE_POWERUP_ID="..." # Power-Up ID (optional)
VITE_MAILCHIMP_LINK="..." # Enables Sentry if presentACT_ENV=dev|prod # Environment
TRELLO_SECRET="<oauth-secret>" # From https://trello.com/app-key
AWS_REGION="eu-west-1" # AWS regionnpm install
npm run dev # Starts Vite on https://localhost:3001Setup Requirements:
- Create Trello team
- Create Power-Up at https://trello.com/power-ups/admin
- Set iframe URL to
https://localhost:3001/ - Enable capabilities: Board buttons, Card badges, Card buttons, Card back section, Show settings
- Accept self-signed certificate in browser
Frontend: GitHub Actions builds and CDK deploys to S3+CloudFront Backend: CDK synth + deploy creates Lambda functions and APIs
npm run lint # ESLint
npm run lint:fix # Auto-fix
npm run analyze # TypeScript checking
npm run build # Production build- Power-Up: Trello's term for browser extensions/integrations
- Capability: Extension point provided by Trello (e.g., card-badges)
- Shared Storage: Data visible to all board members
- Private Storage: Data visible only to the member who saved it
- Range: Completed time tracking session (start + end time)
- Timer: Currently running time tracker (start time only)
- Estimate: Expected time for a card (set by each member)
- Auto-Timer: Feature that auto-starts timer when card moved to specific list
- Threshold: Minimum time required to save a tracking (prevents accidental short entries)
- Webhook: HTTP callback from Trello when events occur (card moved, etc.)
-
Data is in Trello: Never suggest external databases for card data. Everything is stored in Trello's plugin data system.
-
Storage is Limited: Always consider the 4KB limit. Suggest compression techniques.
-
Member Context: Most operations need
memberId. It's cached intrello.ts. -
One Timer Rule: Starting a timer must stop all other timers for that member.
-
Authorization Modes: Users can be unauthorized (basic features), read-only (exports), or read-write (webhooks).
-
WebSocket is Optional: Auto-timer feature requires WebSocket + webhook setup. Core features work without it.
-
Premium Features: Export and calendar are gated. Check for subscription in code.
-
Incognito Support: App must work in incognito (no localStorage, no auth).
-
Theme-Aware: UI adapts to Trello's light/dark theme dynamically.
-
Error Recovery: Storage errors should show helpful alerts, not crash the app.
Activity Timer includes a comprehensive onboarding system that helps users discover features without being intrusive.
- Adaptive Help Buttons: Prominent for new users (0-2 days), subtle for active users (2+ days)
- Trello Native Modals: Uses Trello's modal system for help content (no external UI libraries)
- Centralized Content: All help text in
src/utils/help-content.ts - Private Tracking: User's help interaction stored in Trello member private storage
- AuthSplash Component: Unified authorization screens for premium features with integrated help
src/components/HelpButton.vue- Adaptive help buttonsrc/components/AuthSplash.vue- Authorization splash with helpsrc/components/onboarding.ts- State trackingsrc/pages/HelpPage.vue- Help content display (in Trello modal)src/utils/help-content.ts- All help content
For complete implementation details, see docs/onboarding.md.
- Trello Power-Up Docs: https://developer.atlassian.com/cloud/trello/power-ups/
- Trello API Reference: https://developer.atlassian.com/cloud/trello/rest/
- AWS CDK Docs: https://docs.aws.amazon.com/cdk/
- PrimeVue Docs: https://primevue.org/
This document is maintained by the Activity Timer team. Last updated: 2025-10-01