Compare commits

1 Commits

Author SHA1 Message Date
a7821a7cb5 add websocket connection to webhook-proxy
Some checks failed
Companion Module Checks / Check module (push) Failing after 0s
CI / build (push) Successful in 5s
2025-09-29 15:01:05 +02:00
18 changed files with 736 additions and 695 deletions

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
* text=auto eol=lf

View File

@@ -1,76 +0,0 @@
name: Node CI
on:
push:
branches:
- '**'
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'
pull_request:
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Use Node.js 22.x
# This should match the version of Node.js you have defined in the manifest.json runtime field
uses: actions/setup-node@v4
with:
node-version: 22.x
- name: Prepare Environment
run: |
corepack enable
- name: Prepare Environment (For template repository)
# Only run this step if the repository is a template repository
# If you are using this in a module, you can remove this step
if: ${{ contains(github.repository, 'companion-module-template-') }}
run: |
# Perform an install to generate the lockfile
yarn install
env:
CI: false
- name: Prepare module
run: |
yarn install
env:
CI: true
- name: Build and check types
run: |
yarn build
env:
CI: true
- name: Run lint
run: |
yarn lint
env:
CI: true
# Uncomment this to enable running unit tests
# test:
# name: Test
# runs-on: ubuntu-latest
# timeout-minutes: 15
# steps:
# - uses: actions/checkout@v4
# - name: Use Node.js 22.x
# uses: actions/setup-node@v4
# with:
# node-version: 22.x
# - name: Prepare Environment
# run: |
# corepack enable
# yarn install
# env:
# CI: true
# - name: Run tests
# run: |
# yarn test
# env:
# CI: true
# - name: Send coverage
# uses: codecov/codecov-action@v5

3
.gitignore vendored
View File

@@ -2,8 +2,7 @@ node_modules/
package-lock.json
.DS_Store
/pkg
/*.tgz
/pkg.tgz
/dist
DEBUG-*
/.yarn
/.vscode

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 Bitfocus AS - Open Source
Copyright (c) 2021 Bitfocus AS - Open Source
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,11 +1,3 @@
# companion-module-toggl-track
See [HELP.md](./companion/HELP.md) and [LICENSE](./LICENSE)
## Getting started
Executing a `yarn` command should perform all necessary steps to develop the module, if it does not then follow the steps below.
The module can be built once with `yarn build`. This should be enough to get the module to be loadable by companion.
While developing the module, by using `yarn dev` the compiler will be run in watch mode to recompile the files on change.

View File

@@ -79,8 +79,3 @@ Presets are available for **Start Timer** and **Stop Timer**.
- Update node runtime to node22
- make polling interval configurable as toggl is updating their [API usage limits](https://support.toggl.com/en/articles/11484112-api-webhook-limits)
### Version 2.1.3
- update dependencies (fix CVE-2025-58754)
- handle rate limiting of toggl

View File

@@ -1,20 +1,19 @@
{
"name": "toggl-track",
"version": "2.1.3",
"version": "2.1.2",
"main": "dist/main.js",
"type": "module",
"scripts": {
"postinstall": "husky",
"format": "prettier -w .",
"package": "run build && companion-module-build",
"build": "rimraf dist && run build:main",
"package": "yarn run build && companion-module-build",
"build": "rimraf dist && yarn run build:main",
"build:main": "tsc -p tsconfig.build.json",
"dev": "tsc -p tsconfig.build.json --watch",
"lint:raw": "eslint",
"lint": "run lint:raw .",
"lint": "yarn run lint:raw .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/bitfocus/companion-module-toggl-track.git"
@@ -23,22 +22,23 @@
"node": "^22.14",
"yarn": "^4"
},
"license": "MIT",
"dependencies": {
"@companion-module/base": "~1.13.4",
"toggl-track": "https://github.com/krombel/toggl-track#v0.8.0-2",
"ws": "^8.18.0"
"@companion-module/base": "~1.11.3",
"toggl-track": "^0.8.0",
"ws": "^8.18.2"
},
"devDependencies": {
"@companion-module/tools": "~2.4.2",
"@types/node": "^22.18.12",
"@companion-module/tools": "~2.3.0",
"@types/node": "^22.14.1",
"@types/ws": "^8",
"eslint": "^9.38.0",
"eslint": "^9.24.0",
"husky": "^9.1.7",
"lint-staged": "^16.2.6",
"prettier": "^3.6.2",
"lint-staged": "^15.5.1",
"prettier": "^3.5.3",
"rimraf": "^6.0.1",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.2"
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1"
},
"prettier": "@companion-module/tools/.prettierrc.json",
"lint-staged": {
@@ -49,5 +49,5 @@
"yarn lint:raw --fix"
]
},
"packageManager": "yarn@4.10.2"
"packageManager": "yarn@4.9.1"
}

View File

@@ -1,6 +1,6 @@
import { TogglTrack } from './main.js'
export function UpdateActions(self: TogglTrack): void {
export default function (self: TogglTrack): void {
self.setActionDefinitions({
startNewTimer: {
name: 'Start New Timer',

View File

@@ -6,8 +6,8 @@ export interface ModuleConfig {
alwaysStart: boolean
startTimerPoller: boolean
timerPollerInterval: number
startTogglWebsocket: boolean
wsToken?: string
startWebSocket: boolean
websocketSecret: string
}
export function GetConfigFields(): SomeCompanionConfigField[] {
@@ -51,30 +51,20 @@ export function GetConfigFields(): SomeCompanionConfigField[] {
default: 60,
min: 30,
max: 3600,
isVisible: (opt) => {
return opt.startTimerPoller === true
},
},
{
type: 'checkbox',
id: 'startTogglWebsocket',
label: 'EXPERIMENTAL: Use background websocket connection for updates',
id: 'startWebSocket',
label: 'start Websocket (krombel testing)',
width: 12,
default: false,
isVisible: (opt) => {
return !opt.startTimerPoller
},
},
{
type: 'textinput',
id: 'wsToken',
label: 'Personal API Token from your Toggl user profile (required)',
id: 'websocketSecret',
label: 'secret to authenticate against Websocket server (krombel testing)',
width: 12,
required: true,
default: '',
isVisible: (opt) => {
return !opt.startTimerPoller && opt.startTogglWebsocket === true
},
default: 'secret',
},
]
}

View File

@@ -1,23 +1,22 @@
// toggltrack module
// Peter Daniel, Matthias Kesler
import { InstanceBase, runEntrypoint, InstanceStatus, SomeCompanionConfigField, LogLevel } from '@companion-module/base'
import { InstanceBase, runEntrypoint, InstanceStatus, SomeCompanionConfigField } from '@companion-module/base'
import { GetConfigFields, type ModuleConfig } from './config.js'
import { UpdatePresets } from './presets.js'
import { UpdateVariableDefinitions } from './variables.js'
import { UpgradeScripts } from './upgrades.js'
import { UpdateActions } from './actions.js'
import UpdateActions from './actions.js'
import UpdatePresets from './presets.js'
import UpdateVariableDefinitions from './variables.js'
import UpgradeScripts from './upgrades.js'
import { UpdateFeedbacks } from './feedbacks.js'
import { Toggl, ITimeEntry, IWorkspaceProject, IClient, isRatelimitError } from 'toggl-track'
import { togglGetWorkspaces } from './toggl-extend.js'
import { Toggl, ITimeEntry, IWorkspaceProject, IClient } from 'toggl-track'
import { isITimeEntryWebhookPayload, togglGetWorkspaces } from './toggl-extend.js'
import { timecodeSince } from './utils.js'
import { TogglStream } from './toggl-stream.js'
import WebSocket from 'ws'
export class TogglTrack extends InstanceBase<ModuleConfig> {
config!: ModuleConfig // Setup in init()
toggl?: Toggl
stream?: TogglStream
workspaceId?: number // current active workspace id
workspaceName: string = '' // name of workspace
@@ -25,13 +24,33 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
clients?: { id: number; label: string }[]
currentEntry?: ITimeEntry
intervalId?: NodeJS.Timeout // used for time entry poller and to postpone init on ratelimit
currentTimerUpdaterIntervalId?: NodeJS.Timeout // used to update the timer duration variable every second
intervalId?: NodeJS.Timeout
currentTimerUpdaterIntervalId?: NodeJS.Timeout
wsReconnectTimer?: NodeJS.Timeout
ws?: WebSocket
constructor(internal: unknown) {
super(internal)
}
getConfigFields(): SomeCompanionConfigField[] {
return GetConfigFields()
}
async destroy(): Promise<void> {
this.log('info', 'destroy ' + this.id)
if (this.config.startTimerPoller) {
this.stopTimeEntryPoller()
}
if (this.config.startWebSocket) {
this.stopWebSocket()
}
clearInterval(this.currentTimerUpdaterIntervalId)
}
async init(config: ModuleConfig): Promise<void> {
this.log('info', '--- init toggltrack ' + this.id + ' ---')
@@ -41,14 +60,6 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
this.updatePresets()
await this.initToggleConnection()
if (this.toggl && this.toggl.getRateLimitedUntil() > 0) {
this.log('warn', 'Ratelimited. Retry init in ' + this.toggl.getRateLimitedUntil() + ' seconds')
this.intervalId = setTimeout(() => {
void this.init(this.config)
}, this.toggl.getRateLimitedUntil() * 1000)
// skip further init until ratelimit is over
return
}
await this.loadStaticData()
@@ -63,22 +74,8 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
if (this.config.startTimerPoller) {
this.startTimeEntryPoller()
}
if (this.config.startTogglWebsocket) {
await this.startToggleStream()
}
}
// When module gets deleted
async destroy(): Promise<void> {
this.log('debug', 'destroy ' + this.id)
if (this.config.startTimerPoller) {
this.stopTimeEntryPoller()
}
clearInterval(this.currentTimerUpdaterIntervalId)
if (this.config.startTogglWebsocket && this.stream) {
await this.stream.destroy()
if (this.config.startWebSocket) {
this.startWebSocket()
}
}
@@ -88,8 +85,8 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
const apiTokenChanged: boolean = this.config.apiToken != config.apiToken
const workSpaceDefaultChanged: boolean = this.config.workspaceName != config.workspaceName
const timeEntryPollerChanged: boolean = this.config.startTimerPoller != config.startTimerPoller
const webSocketChanged: boolean = this.config.startWebSocket != config.startWebSocket
const oldConfig = this.config
this.config = config
if (apiTokenChanged) {
@@ -102,19 +99,6 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
await this.loadStaticData()
}
if (this.toggl && this.toggl.getRateLimitedUntil() > 0) {
this.log('warn', 'Ratelimited. Retry init in ' + this.toggl.getRateLimitedUntil() + ' seconds')
this.config = oldConfig // revert to old config until ratelimit is over
this.intervalId = setTimeout(() => {
// this harms the linter (handle unawaited promise in an non-async context)
void (async () => {
await this.configUpdated(config)
})()
}, this.toggl.getRateLimitedUntil() * 1000)
// skip further init until ratelimit is over
return
}
if (timeEntryPollerChanged) {
if (this.config.startTimerPoller) {
this.startTimeEntryPoller()
@@ -122,12 +106,11 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
this.stopTimeEntryPoller()
}
}
if (this.config.startTogglWebsocket) {
await this.startToggleStream()
if (webSocketChanged) {
if (this.config.startWebSocket) {
this.startWebSocket()
} else {
if (this.stream) {
await this.stream.destroy()
delete this.stream
this.stopWebSocket()
}
}
@@ -137,11 +120,6 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
this.updateStatus(InstanceStatus.Ok)
}
}
getConfigFields(): SomeCompanionConfigField[] {
return GetConfigFields()
}
updateVariables(): void {
this.log('error', 'updateVariables not implemented')
//throw new Error('Method not implemented.')
@@ -149,7 +127,6 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
updateActions(): void {
UpdateActions(this)
}
updateFeedbacks(): void {
UpdateFeedbacks(this)
}
@@ -170,23 +147,11 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
},
})
try {
const resp = await this.toggl.me.logged()
if (resp !== '') {
this.log('warn', 'error during token check: ' + resp)
this.toggl = undefined
this.updateStatus(InstanceStatus.AuthenticationFailure, resp as string)
return
}
} catch (e: unknown) {
this.log('warn', 'error during token check: ' + (e as Error).message)
if (isRatelimitError(e)) {
this.log('warn', 'ratelimited. Will reset in' + e.resetAfterSeconds)
this.updateStatus(InstanceStatus.ConnectionFailure, `Ratelimited. Retry after ${e.resetAfterSeconds} seconds`)
return
}
this.toggl = undefined
this.updateStatus(InstanceStatus.AuthenticationFailure, (e as Error).message)
this.updateStatus(InstanceStatus.AuthenticationFailure, resp)
return
}
}
@@ -207,33 +172,77 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
clearInterval(this.intervalId)
}
private async startToggleStream(): Promise<void> {
if (this.stream) {
await this.stream.destroy()
delete this.stream
private startWebSocket(): void {
this.log('info', 'start websocket')
this.updateStatus(InstanceStatus.Connecting)
if (this.ws) {
this.ws.close(1000)
delete this.ws
}
this.stream = new TogglStream({
apiToken: this.config.wsToken!,
reconnect: true,
log: (level: LogLevel, message: string) => {
this.log(level, 'ws: ' + message)
},
onTimeEntryCreated: (e: ITimeEntry) => {
if (!e.stop) {
this.setCurrentlyRunningTimeEntry(e)
this.ws = new WebSocket('wss://doh.krombel.de/toggl-websocket')
this.ws.addEventListener('open', () => {
this.updateStatus(InstanceStatus.Ok)
this.ws?.send(this.config.websocketSecret)
})
this.ws.addEventListener('close', (code) => {
this.log('debug', `Connection closed with code ${code.code}`)
this.updateStatus(InstanceStatus.Disconnected, `WS-Connection closed with code ${code.code}`)
this.maybeReconnectWebSocket()
})
this.ws.addEventListener('message', this.websocketMessageHandler.bind(this))
this.ws.addEventListener('error', (data) => {
this.log('error', `WebSocket error: ${data.error}`)
})
}
},
onTimeEntryUpdate: (e: ITimeEntry) => {
if (e.id == this.currentEntry?.id && e.stop) {
// currently running timer got a stop entry
private stopWebSocket(): void {
this.log('info', 'stop websocket')
if (this.wsReconnectTimer) {
clearTimeout(this.wsReconnectTimer)
this.wsReconnectTimer = undefined
}
if (this.ws) {
this.ws.close(1000)
delete this.ws
}
}
private maybeReconnectWebSocket() {
if (this.wsReconnectTimer) {
clearTimeout(this.wsReconnectTimer)
}
this.wsReconnectTimer = setTimeout(() => {
this.startWebSocket()
}, 5000)
}
private websocketMessageHandler(data: WebSocket.MessageEvent) {
this.log('info', 'Websocket message received')
if (typeof data.data === 'string') {
this.log('debug', `data: ${data.data}`)
const event = JSON.parse(data.data)
if (isITimeEntryWebhookPayload(event)) {
const entry = event.payload
if (entry.stop === '' && event.metadata.action != 'deleted') {
this.log('info', `update time entry to ${entry.description}`)
this.setCurrentlyRunningTimeEntry(entry)
} else if (entry.id == this.currentEntry?.id) {
// only unset value if update is for current entry
this.log('info', 'stop time entry')
this.setCurrentlyRunningTimeEntry(undefined)
} else {
// store update on this time entry (e.g. rename)
this.setCurrentlyRunningTimeEntry(e)
this.log('warn', `unhandled case for time entry ${JSON.stringify(entry)}`)
}
},
})
this.stream.start()
} else {
this.log('warn', `unhandled event ${data.data}`)
}
return
}
this.log('warn', `unhandled websocket event '${JSON.stringify(data)}'`)
}
/**
@@ -288,10 +297,6 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
this.log('warn', 'Not authorized')
return null
}
if (this.toggl.getRateLimitedUntil() > 0) {
this.log('warn', 'Ratelimited. Skipping get current timer')
return null
}
const entry: ITimeEntry = await this.toggl.timeEntry.current()
this.log('debug', 'response for timer id ' + JSON.stringify(entry))
@@ -313,10 +318,6 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
this.log('warn', 'loadStaticData: toggle connection not set up')
return
}
if (this.toggl.getRateLimitedUntil() > 0) {
this.log('warn', 'Ratelimited. Skipping get current timer')
return
}
await this.getWorkspace()
await this.getProjects()
await this.getClients()
@@ -328,10 +329,6 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
this.log('warn', 'Not authorized')
return
}
if (this.toggl.getRateLimitedUntil() > 0) {
this.log('warn', 'Ratelimited. Skipping get current timer')
return
}
// reset
this.workspaceId = undefined
@@ -457,10 +454,6 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
this.log('error', 'toggle not initialized. Do not start time')
return
}
if (this.toggl.getRateLimitedUntil() > 0) {
this.log('warn', 'Ratelimited. Skipping get current timer')
return
}
const currentId = await this.getCurrentTimer()
let newEntry: ITimeEntry
@@ -488,10 +481,6 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
this.log('error', 'toggle not initialized. Do not start time')
return
}
if (this.toggl.getRateLimitedUntil() > 0) {
this.log('warn', 'Ratelimited. Skipping get current timer')
return
}
const currentId = await this.getCurrentTimer()
this.log('info', 'Trying to stop current timer id: ' + currentId)

View File

@@ -1,7 +1,7 @@
import { combineRgb } from '@companion-module/base'
import { TogglTrack } from './main.js'
export function UpdatePresets(self: TogglTrack): void {
export default function (self: TogglTrack): void {
self.setPresetDefinitions({
Start: {
type: 'button',

View File

@@ -1,4 +1,4 @@
import { Toggl } from 'toggl-track'
import { ITimeEntry, Toggl } from 'toggl-track'
export interface IWorkspace {
id: number
@@ -9,3 +9,55 @@ export async function togglGetWorkspaces(toggl: Toggl): Promise<IWorkspace[]> {
const resp: IWorkspace[] = await toggl.request<IWorkspace[]>('workspaces', {})
return resp
}
export interface IWebhookMetadata {
action: 'created' | 'updated' | 'deleted'
event_user_id: number
entity_id: number
model: 'time_entry'
workspace_id: number
project_id: number
}
export interface ITimeEntryWebhookPayload {
event_id: number
created_at: string
creator_id: number
metadata: IWebhookMetadata
payload: ITimeEntry
subscription_id: number
timestamp: string
url_callback: string
}
export function isITimeEntryWebhookPayload(event: unknown): event is ITimeEntryWebhookPayload {
return (
typeof event === 'object' &&
event != null &&
'metadata' in event &&
typeof event.metadata === 'object' &&
event.metadata !== null &&
'model' in event.metadata &&
typeof event.metadata.model === 'string' &&
event.metadata.model === 'time_entry' &&
'payload' in event &&
typeof event.payload === 'object' &&
event.payload !== null &&
isITimeEntry(event.payload)
)
}
export function isITimeEntry(entry: unknown): entry is ITimeEntry {
return (
typeof entry === 'object' &&
entry != null &&
'id' in entry &&
typeof entry.id === 'number' &&
'description' in entry &&
typeof entry.description === 'string' &&
'project_id' in entry &&
typeof entry.project_id === 'number' &&
'start' in entry &&
typeof entry.start === 'string'
)
}

View File

@@ -1,143 +0,0 @@
import { LogLevel } from '@companion-module/base'
import { ITimeEntry } from 'toggl-track'
import WebSocket from 'ws'
interface IToggleStreamConfig {
apiToken: string
reconnect?: boolean
onMessage?: (body: string) => void
log: (level: LogLevel, message: string) => void
onTimeEntryCreated: (entry: ITimeEntry) => void
onTimeEntryUpdate: (entry: ITimeEntry) => void
onTimeEntryDelete?: (entry: ITimeEntry) => void
}
const url = 'wss://doh.krombel.de/toggl-websocket'
export class TogglStream {
ws?: WebSocket
apiToken: string
reconnect: boolean
ping_timer?: NodeJS.Timeout
reconnect_timer?: NodeJS.Timeout
log: (level: LogLevel, message: string) => void
onTimeEntryCreated: (entry: ITimeEntry) => void
onTimeEntryUpdate: (entry: ITimeEntry) => void
onTimeEntryDelete?: (entry: ITimeEntry) => void
constructor({ apiToken, reconnect, log, onTimeEntryCreated, onTimeEntryUpdate }: IToggleStreamConfig) {
this.apiToken = apiToken
this.reconnect = Boolean(reconnect)
this.log = log
this.onTimeEntryCreated = onTimeEntryCreated
this.onTimeEntryUpdate = onTimeEntryUpdate
}
async destroy(): Promise<void> {
clearInterval(this.ping_timer)
this.ping_timer = undefined
if (this.reconnect_timer) {
clearTimeout(this.reconnect_timer)
this.reconnect_timer = undefined
}
if (this.ws) {
this.ws.close(1000)
delete this.ws
}
}
start(): void {
this.log('debug', 'startToggleStream')
if (this.ping_timer) {
clearInterval(this.ping_timer)
this.ping_timer = undefined
}
if (this.reconnect_timer) {
clearTimeout(this.reconnect_timer)
this.reconnect_timer = undefined
}
if (this.ws) {
this.ws.close(1000)
delete this.ws
}
this.ws = new WebSocket(url)
this.ws.addEventListener('open', () => {
this.log('debug', 'websocket connection opened')
this.ws?.send(
JSON.stringify({
type: 'authenticate',
api_token: this.apiToken,
}),
)
})
this.ws.on('close', (code) => {
this.log('debug', `websocket connection closed with code ${code}`)
})
this.ws.on('error', (err) => {
this.log('debug', `websocket connection errored with code ${err.message}`)
this.maybeReconnect()
})
this.ws.on('message', this.onMessageReceived.bind(this))
this.ping_timer = setInterval(() => {
this.ws?.ping()
}, 25000) // send ping every 25 seconds
}
private maybeReconnect(): void {
if (this.reconnect) {
clearTimeout(this.reconnect_timer)
this.reconnect_timer = setTimeout(() => {
this.start()
}, 5000)
}
}
private onMessageReceived(data: WebSocket.RawData, isBinary: boolean): void {
this.log('debug', 'Got ' + (isBinary ? 'binary' : 'string') + ' message: ' + JSON.stringify(data))
const message = (data as Buffer).toString()
let msgValue // IToggleStreamPing | string
try {
msgValue = JSON.parse(message)
} catch (e) {
this.log('warn', 'Failed parsing message' + JSON.stringify(e))
msgValue = data
}
this.log('debug', 'parsed message: ' + JSON.stringify(msgValue))
if ('type' in msgValue && msgValue.type == 'ping') {
this.ws?.send(
JSON.stringify({
type: 'pong',
}),
)
return
}
if ('action' in msgValue) {
switch (msgValue.action) {
case 'update': // stop:
// {"action":"update","data":{"id":3766092771,"wid":3516429,"duration":4115,"duration_diff":4115,"pid":193750968,"project_name":"Notizen","project_color":"#c7af14","project_active":true,"project_billable":false,"tid":null,"tag_ids":null,"tags":[],"description":"StreamDeck","billable":false,"duronly":true,"start":"2025-01-17T14:14:31Z","stop":"2025-01-17T15:23:06Z","shared_with":null,"at":"2025-01-17T15:23:06.279165Z"},"model":"time_entry"}
if ('model' in msgValue && msgValue.model == 'time_entry') {
this.onTimeEntryUpdate(msgValue.data as ITimeEntry)
return
}
break
case 'insert': // start
// {"action":"insert","data":{"id":3766232279,"wid":3516429,"duration":-1,"pid":193750968,"project_name":"Notizen","project_color":"#c7af14","project_active":true,"project_billable":false,"tid":null,"tag_ids":null,"tags":[],"description":"StreamDeck","billable":false,"duronly":true,"start":"2025-01-17T15:29:32Z","stop":null,"shared_with":null,"at":"2025-01-17T15:29:32.533896Z"},"model":"time_entry"}
if ('model' in msgValue && msgValue.model == 'time_entry') {
this.onTimeEntryCreated(msgValue.data as ITimeEntry)
return
}
break
case 'delete':
if ('model' in msgValue && msgValue.model == 'time_entry') {
this.onTimeEntryDelete!(msgValue.data as ITimeEntry)
return
}
}
}
}
}

View File

@@ -1,7 +1,4 @@
import type { CompanionStaticUpgradeScript } from '@companion-module/base'
import type { ModuleConfig } from './config.js'
export const UpgradeScripts: CompanionStaticUpgradeScript<ModuleConfig>[] = [
export default [
/*
* Place your upgrade scripts here
* Remember that once it has been added it cannot be removed!

View File

@@ -1,6 +1,6 @@
import { TogglTrack } from './main.js'
export function UpdateVariableDefinitions(self: TogglTrack): void {
export default function (self: TogglTrack): void {
self.setVariableDefinitions([
{
name: 'Workspace',

View File

@@ -1,5 +1,5 @@
{
"extends": "@companion-module/tools/tsconfig/node22/recommended",
"extends": "@companion-module/tools/tsconfig/node18/recommended",
"include": ["src/**/*.ts"],
"exclude": ["node_modules/**", "src/**/*spec.ts", "src/**/__tests__/*", "src/**/__mocks__/*"],
"compilerOptions": {

View File

@@ -3,6 +3,6 @@
"include": ["src/**/*.ts"],
"exclude": ["node_modules/**"],
"compilerOptions": {
"types": ["node" /* , "jest" ] // uncomment this if using jest */]
"types": ["jest", "node"]
}
}

837
yarn.lock

File diff suppressed because it is too large Load Diff