diff --git a/src/js/src/app.ts b/src/js/src/app.ts index b7e709d..dd6dbe0 100644 --- a/src/js/src/app.ts +++ b/src/js/src/app.ts @@ -1,12 +1,14 @@ import { initTrackerAction } from "./lib/action/tracker-action"; import { KimaiApi } from "./lib/api/kimai-api"; import { AppEvent, StateKey } from "./lib/constants"; +import { registerGlobalErrorReporter } from "./lib/error-reporter"; import { Store } from "./lib/store/store"; -import { AppState, GlobalSettings } from "./lib/types"; +import { AppState, GlobalSettings, SDConnectionInfo } from "./lib/types"; const store = new Store(); -$SD.onConnected(() => { +$SD.onConnected((args: SDConnectionInfo) => { + registerGlobalErrorReporter(args); $SD.getGlobalSettings(); $SD.onDidReceiveGlobalSettings( (event: { payload: { settings: GlobalSettings } }) => { @@ -23,6 +25,9 @@ $SD.onConnected(() => { } } ); + // setTimeout(() => { + // throw new Error("test error " + new Date().toISOString()); + // }, 1000); }); EventEmitter.on(AppEvent.actionSuccess, (context: string) => diff --git a/src/js/src/lib/api/api.ts b/src/js/src/lib/api/api.ts index 5a89e34..18893d6 100644 --- a/src/js/src/lib/api/api.ts +++ b/src/js/src/lib/api/api.ts @@ -13,10 +13,8 @@ export type ApiResponse = export type TrackingItem = { id: string | number; -}; - -export type TimeEntry = { duration: number; + begin: string; }; export type Category = { diff --git a/src/js/src/lib/api/kimai-api-tracker-connector.ts b/src/js/src/lib/api/kimai-api-tracker-connector.ts index 0764a30..3778c36 100644 --- a/src/js/src/lib/api/kimai-api-tracker-connector.ts +++ b/src/js/src/lib/api/kimai-api-tracker-connector.ts @@ -1,9 +1,10 @@ +import { parse } from "date-fns"; import { AppEvent, StateKey } from "../constants"; import { Store } from "../store/store"; import { Tracker, TrackerEvent } from "../tracker"; import { AppState, KimaiBackendProviderPluginConfig } from "../types"; import { ApiTrackerConnector } from "./api"; -import { KimaiApi } from "./kimai-api"; +import { KimaiApi, kimaiDateFormatTz } from "./kimai-api"; export class KimaiApiTrackerConnector implements ApiTrackerConnector { #api: KimaiApi; @@ -13,6 +14,7 @@ export class KimaiApiTrackerConnector implements ApiTrackerConnector { onStart: this.onStart.bind(this), onStop: this.onStop.bind(this), onRequestWorkedToday: this.onRequestWorkedToday.bind(this), + onCheckIfCurrentlyActive: this.onCheckIfCurrentlyActive.bind(this), }; backendProvider = "kimai" as const; @@ -70,6 +72,30 @@ export class KimaiApiTrackerConnector implements ApiTrackerConnector { } } + async onCheckIfCurrentlyActive() { + console.log( + "KimaiApiTrackerConnector.onCheckIfCurrentlyActive()", + this.settings(this.#tracker) + ); + const projectId = this.settings(this.#tracker)?.projectId; + const activityId = this.settings(this.#tracker)?.activityId; + if (this.#tracker.running) { + throw new Error( + "Tracker has already been started in Stream Deck. Check if there's an active event in the backend is too late." + ); + } + if (projectId && activityId) { + const event = await this.#api.getCurrentlyActive(projectId, activityId); + if (event) { + this.#store.patchState({ + [StateKey.currentEvent]: event, + }); + const startTime = parse(event.begin, kimaiDateFormatTz, new Date()); + this.#tracker.start(startTime); + } + } + } + connect(tracker: Tracker) { this.#tracker = tracker; @@ -79,6 +105,10 @@ export class KimaiApiTrackerConnector implements ApiTrackerConnector { TrackerEvent.requestWorkedToday, this.#bound.onRequestWorkedToday ); + tracker.addEventListener( + TrackerEvent.checkIfCurrentlyActive, + this.#bound.onCheckIfCurrentlyActive + ); return this; } diff --git a/src/js/src/lib/api/kimai-api.ts b/src/js/src/lib/api/kimai-api.ts index 1530b43..092b6b8 100644 --- a/src/js/src/lib/api/kimai-api.ts +++ b/src/js/src/lib/api/kimai-api.ts @@ -1,11 +1,5 @@ import { format, startOfToday } from "date-fns"; -import { - ApiResponse, - Category, - TimeEntry, - TrackingItem, - tryFetch, -} from "./api"; +import { ApiResponse, Category, TrackingItem, tryFetch } from "./api"; import { KimaiBackendProviderPluginConfig } from "../types"; type ApiConfig = { @@ -14,6 +8,9 @@ type ApiConfig = { token: string; }; +export const kimaiDateFormat = "yyyy-MM-dd'T'HH:mm:ss"; +export const kimaiDateFormatTz = "yyyy-MM-dd'T'HH:mm:ssxx"; + export class KimaiApi { static instance: KimaiApi | null = null; @@ -87,7 +84,7 @@ export class KimaiApi { this.assertValidConfig(); const url = `${this.#baseUrl}api/timesheets`; const body = { - begin: format(new Date(), "yyyy-MM-dd'T'HH:mm:ss"), + begin: format(new Date(), kimaiDateFormat), project: projectId, activity: activityId, description: "", @@ -110,6 +107,19 @@ export class KimaiApi { return tryFetch(url, options); } + async getCurrentlyActive(projectId: number, activityId: number) { + this.assertValidConfig(); + const params = new URLSearchParams({ + active: "1", + project: String(projectId), + activity: String(activityId), + }); + const url = `${this.#baseUrl}api/timesheets?${params.toString()}`; + const response = await tryFetch(url, this.fetchOptions); + if (!response.success) return null; + return response.body.length ? response.body[0] : null; + } + async getProjects() { this.assertValidConfig(); const url = `${this.#baseUrl}api/projects`; @@ -128,7 +138,7 @@ export class KimaiApi { async listTodaysTimeEntries( projectId: number, activityId: number - ): Promise> { + ): Promise> { this.assertValidConfig(); if (!projectId || !activityId) { return { @@ -139,11 +149,11 @@ export class KimaiApi { }; } const params = new URLSearchParams({ - begin: format(startOfToday(), "yyyy-MM-dd'T'HH:mm:ss"), + begin: format(startOfToday(), kimaiDateFormat), "projects[]": String(projectId), "activities[]": String(activityId), }); const url = `${this.#baseUrl}api/timesheets?${params.toString()}`; - return tryFetch(url, this.fetchOptions); + return tryFetch(url, this.fetchOptions); } } diff --git a/src/js/src/lib/error-reporter.ts b/src/js/src/lib/error-reporter.ts new file mode 100644 index 0000000..63550d5 --- /dev/null +++ b/src/js/src/lib/error-reporter.ts @@ -0,0 +1,9 @@ +import { SDConnectionInfo } from "./types"; + +export function registerGlobalErrorReporter(info: SDConnectionInfo) { + window.addEventListener("error", ({ error }: ErrorEvent) => { + const message = `Error in ${info?.appInfo?.plugin?.uuid} (${info?.appInfo?.plugin?.version}): ${error?.message}\n${error?.stack}`; + console.log("Writing error log:", message); + $SD.logMessage(message); + }); +} diff --git a/src/js/src/lib/tracker.ts b/src/js/src/lib/tracker.ts index 623b94e..d342017 100644 --- a/src/js/src/lib/tracker.ts +++ b/src/js/src/lib/tracker.ts @@ -5,6 +5,7 @@ export const TrackerEvent = { start: "start", stop: "stop", requestWorkedToday: "requestWorkedToday", + checkIfCurrentlyActive: "checkIfCurrentlyActive", } as const; export class Tracker extends EventTarget { @@ -37,6 +38,7 @@ export class Tracker extends EventTarget { public running: boolean = false; public workedToday: number | undefined; private interval: number | undefined; + private checkedIfCurrentlyActive: boolean = false; constructor(context: string, running: boolean) { console.log("Tracker.constructor(context, running)", { context, running }); @@ -52,6 +54,10 @@ export class Tracker extends EventTarget { if (typeof this.workedToday !== "number") { this.dispatchEvent(new Event(TrackerEvent.requestWorkedToday)); } + if (!this.checkedIfCurrentlyActive) { + this.dispatchEvent(new Event(TrackerEvent.checkIfCurrentlyActive)); + this.checkedIfCurrentlyActive = true; + } } public get settings(): PluginSettings | undefined { return this.#settings; @@ -62,14 +68,19 @@ export class Tracker extends EventTarget { return Math.floor((new Date().getTime() - this.startTime?.getTime()) / 1e3); } - start() { + start(startTime?: Date) { + console.log("Tracker.start(startTime)", { startTime }); this.running = true; - this.startTime = new Date(); + this.startTime = startTime ?? new Date(); this.render(); Tracker.pauseOtherTrackers(this); - this.dispatchEvent(new Event(TrackerEvent.requestWorkedToday)); - this.dispatchEvent(new Event(TrackerEvent.start)); this.interval = window.setInterval(this.tick(), 6e4); + if (!startTime) { + this.dispatchEvent(new Event(TrackerEvent.requestWorkedToday)); + this.dispatchEvent(new Event(TrackerEvent.start)); + } else { + // A given start time means that the tracker was started in the backend. + } } tick() {