Skip to content

Commit 092d8d2

Browse files
ikraamgclaude
andcommitted
Updated schedule execution to return detailed webhook results
Necessary to provide granular user feedback when webhook delivery fails while screenshot capture succeeds. Previously, the UI only showed binary success/failure states. Changes: - Added WebhookResult type with status code, URL, and error details - Refactored ScheduleExecutor to return webhook outcome separately - Enhanced UI to show "Partial Success" when screenshot saves but webhook fails, with full error context in modal - Added browser console logging for debugging webhook issues 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c5c4bbf commit 092d8d2

4 files changed

Lines changed: 201 additions & 39 deletions

File tree

trmnl-ha/ha-trmnl/html/js/app.ts

Lines changed: 101 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
Schedule,
2929
CropRegion,
3030
ScheduleUpdate,
31+
SendScheduleResponse,
3132
} from '../../types/domain.js'
3233

3334
// =============================================================================
@@ -264,29 +265,18 @@ class App {
264265
const sendCommand = new SendSchedule()
265266
const result = await sendCommand.call(scheduleId)
266267

267-
if (result.success) {
268-
button.textContent = '✓ Sent!'
269-
button.style.backgroundColor = '#10b981'
270-
button.style.opacity = '1'
268+
// Log detailed result to browser console
269+
this.#logSendResult(result)
271270

272-
await this.#confirmModal.alert({
273-
title: '✓ Success!',
274-
message: 'Screenshot captured and sent to webhook successfully!',
275-
type: 'success',
276-
})
277-
} else {
278-
console.error('Error sending webhook:', result.error)
271+
// Determine overall status based on screenshot + webhook results
272+
const { title, message, type, buttonText, buttonColor } =
273+
this.#buildSendResultFeedback(result)
279274

280-
button.textContent = '✗ Failed'
281-
button.style.backgroundColor = '#ef4444'
282-
button.style.opacity = '1'
275+
button.textContent = buttonText
276+
button.style.backgroundColor = buttonColor
277+
button.style.opacity = '1'
283278

284-
await this.#confirmModal.alert({
285-
title: '✗ Error',
286-
message: `Failed to send webhook: ${result.error ?? 'Unknown error'}`,
287-
type: 'error',
288-
})
289-
}
279+
await this.#confirmModal.alert({ title, message, type })
290280

291281
// Reset button state if it still exists in DOM
292282
if (document.body.contains(button)) {
@@ -298,6 +288,97 @@ class App {
298288
}
299289
}
300290

291+
/** Logs send result to browser console for debugging */
292+
#logSendResult(result: SendScheduleResponse): void {
293+
const logPrefix = '[Send Now]'
294+
295+
if (!result.success) {
296+
console.error(`${logPrefix} Screenshot capture failed:`, result.error)
297+
return
298+
}
299+
300+
console.log(`${logPrefix} Screenshot saved: ${result.savedPath}`)
301+
302+
if (!result.webhook) {
303+
console.log(`${logPrefix} No webhook configured`)
304+
return
305+
}
306+
307+
const { webhook } = result
308+
if (webhook.success) {
309+
console.log(
310+
`${logPrefix} Webhook success: ${webhook.statusCode}${webhook.url}`
311+
)
312+
} else {
313+
console.error(
314+
`${logPrefix} Webhook failed: ${webhook.error}${webhook.url}`
315+
)
316+
}
317+
}
318+
319+
/** Builds user-facing feedback based on send result */
320+
#buildSendResultFeedback(result: SendScheduleResponse): {
321+
title: string
322+
message: string
323+
type: 'success' | 'error' | 'warning'
324+
buttonText: string
325+
buttonColor: string
326+
} {
327+
// Screenshot capture failed
328+
if (!result.success) {
329+
return {
330+
title: 'Screenshot Failed',
331+
message: `Failed to capture screenshot: ${
332+
result.error ?? 'Unknown error'
333+
}`,
334+
type: 'error',
335+
buttonText: 'Failed',
336+
buttonColor: '#ef4444',
337+
}
338+
}
339+
340+
// No webhook configured - just screenshot saved
341+
if (!result.webhook) {
342+
return {
343+
title: 'Screenshot Saved',
344+
message: `Screenshot captured and saved to:\n${result.savedPath}`,
345+
type: 'success',
346+
buttonText: 'Saved',
347+
buttonColor: '#10b981',
348+
}
349+
}
350+
351+
// Webhook was attempted
352+
const { webhook } = result
353+
354+
if (webhook.success) {
355+
return {
356+
title: 'Success',
357+
message:
358+
`Screenshot captured and sent to webhook.\n\n` +
359+
`Saved: ${result.savedPath}\n` +
360+
`Webhook: ${webhook.statusCode} OK\n` +
361+
`URL: ${webhook.url ?? 'N/A'}`,
362+
type: 'success',
363+
buttonText: 'Sent',
364+
buttonColor: '#10b981',
365+
}
366+
}
367+
368+
// Webhook failed but screenshot was saved
369+
return {
370+
title: 'Partial Success',
371+
message:
372+
`Screenshot saved, but webhook delivery failed.\n\n` +
373+
`Saved: ${result.savedPath}\n` +
374+
`Error: ${webhook.error}\n` +
375+
`URL: ${webhook.url ?? 'N/A'}`,
376+
type: 'warning',
377+
buttonText: 'Partial',
378+
buttonColor: '#f59e0b',
379+
}
380+
}
381+
301382
async updateScheduleFromForm(): Promise<void> {
302383
const schedule = this.#scheduleManager.activeSchedule
303384
if (!schedule) return

trmnl-ha/ha-trmnl/lib/http-router.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import {
2323
} from './scheduleStore.js'
2424
import { loadPresets } from '../devices.js'
2525
import type { BrowserFacade } from './browserFacade.js'
26-
import type { ScheduleInput, ScheduleUpdate } from '../types/domain.js'
26+
import type {
27+
ScheduleInput,
28+
ScheduleUpdate,
29+
WebhookResult,
30+
} from '../types/domain.js'
2731
import { toJson } from './json.js'
2832
import { httpLogger } from './logger.js'
2933

@@ -50,7 +54,7 @@ const MIME_TYPES: Record<string, string> = {
5054
interface Scheduler {
5155
executeNow(
5256
scheduleId: string
53-
): Promise<{ success: boolean; savedPath: string }>
57+
): Promise<{ success: boolean; savedPath: string; webhook?: WebhookResult }>
5458
}
5559

5660
/**

trmnl-ha/ha-trmnl/lib/scheduler/schedule-executor.ts

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import {
2020
isSchedulerNetworkError,
2121
} from '../../const.js'
2222
import { loadSchedules } from '../scheduleStore.js'
23-
import type { Schedule, ScreenshotParams } from '../../types/domain.js'
23+
import type {
24+
Schedule,
25+
ScreenshotParams,
26+
WebhookResult,
27+
} from '../../types/domain.js'
2428
import { schedulerLogger } from '../logger.js'
2529

2630
const log = schedulerLogger()
@@ -32,6 +36,7 @@ export type ScreenshotFunction = (params: ScreenshotParams) => Promise<Buffer>
3236
export interface ExecutionResult {
3337
success: boolean
3438
savedPath: string
39+
webhook?: WebhookResult
3540
}
3641

3742
/**
@@ -53,10 +58,23 @@ export class ScheduleExecutor {
5358

5459
const result = await this.#executeWithRetry(schedule)
5560

56-
log.info`Completed: ${schedule.name} in ${Date.now() - startTime}ms`
61+
this.#logResult(schedule.name, result, Date.now() - startTime)
5762
return result
5863
}
5964

65+
/** Logs execution result with full details */
66+
#logResult(name: string, result: ExecutionResult, durationMs: number): void {
67+
const webhookStatus = this.#formatWebhookStatus(result.webhook)
68+
log.info`Completed: ${name} in ${durationMs}ms | saved: ${result.savedPath} | webhook: ${webhookStatus}`
69+
}
70+
71+
/** Formats webhook status for logging */
72+
#formatWebhookStatus(webhook: WebhookResult | undefined): string {
73+
if (!webhook) return 'not configured'
74+
if (webhook.success) return `${webhook.statusCode} OK → ${webhook.url}`
75+
return `FAILED (${webhook.error}) → ${webhook.url}`
76+
}
77+
6078
/** Retry wrapper for network failures */
6179
async #executeWithRetry(schedule: Schedule): Promise<ExecutionResult> {
6280
for (let attempt = 1; attempt <= SCHEDULER_MAX_RETRIES; attempt++) {
@@ -75,13 +93,25 @@ export class ScheduleExecutor {
7593
async #executeOnce(schedule: Schedule): Promise<ExecutionResult> {
7694
const params = buildParams(schedule)
7795
const imageBuffer = await this.#screenshotFn(params)
78-
const savedPath = await this.#saveAndCleanup(schedule, imageBuffer, params.format)
79-
await this.#uploadIfConfigured(schedule, imageBuffer, params.format)
80-
return { success: true, savedPath }
96+
const savedPath = await this.#saveAndCleanup(
97+
schedule,
98+
imageBuffer,
99+
params.format
100+
)
101+
const webhook = await this.#uploadIfConfigured(
102+
schedule,
103+
imageBuffer,
104+
params.format
105+
)
106+
return { success: true, savedPath, webhook }
81107
}
82108

83109
/** Saves screenshot and runs LRU cleanup */
84-
async #saveAndCleanup(schedule: Schedule, imageBuffer: Buffer, format: string): Promise<string> {
110+
async #saveAndCleanup(
111+
schedule: Schedule,
112+
imageBuffer: Buffer,
113+
format: string
114+
): Promise<string> {
85115
const { outputPath } = saveScreenshot({
86116
outputDir: this.#outputDir,
87117
scheduleName: schedule.name,
@@ -91,31 +121,62 @@ export class ScheduleExecutor {
91121
log.info`Saved: ${outputPath}`
92122

93123
const schedules = await loadSchedules()
94-
const maxFiles = schedules.filter((s) => s.enabled).length * SCHEDULER_RETENTION_MULTIPLIER
124+
const maxFiles =
125+
schedules.filter((s) => s.enabled).length * SCHEDULER_RETENTION_MULTIPLIER
95126
const { deletedCount } = cleanupOldScreenshots({
96127
outputDir: this.#outputDir,
97128
maxFiles,
98129
filePattern: SCHEDULER_IMAGE_FILE_PATTERN,
99130
})
100131

101-
if (deletedCount > 0) log.debug`Cleanup: Deleted ${deletedCount} old file(s)`
132+
if (deletedCount > 0)
133+
log.debug`Cleanup: Deleted ${deletedCount} old file(s)`
102134
return outputPath
103135
}
104136

105-
/** Uploads to webhook if configured */
106-
async #uploadIfConfigured(schedule: Schedule, imageBuffer: Buffer, format: string): Promise<void> {
107-
if (!schedule.webhook_url) return
137+
/** Uploads to webhook if configured, returns result for UI feedback */
138+
async #uploadIfConfigured(
139+
schedule: Schedule,
140+
imageBuffer: Buffer,
141+
format: string
142+
): Promise<WebhookResult | undefined> {
143+
if (!schedule.webhook_url) return undefined
144+
145+
const webhookUrl = schedule.webhook_url
108146

109147
try {
110-
await uploadToWebhook({
111-
webhookUrl: schedule.webhook_url,
148+
const result = await uploadToWebhook({
149+
webhookUrl,
112150
webhookHeaders: schedule.webhook_headers,
113151
imageBuffer,
114152
format: format as 'png' | 'jpeg' | 'bmp',
115153
})
154+
155+
log.info`Schedule "${schedule.name}" webhook success: ${result.status} ${result.statusText}`
156+
157+
return {
158+
attempted: true,
159+
success: true,
160+
statusCode: result.status,
161+
url: webhookUrl,
162+
}
116163
} catch (err) {
117-
// Error already logged by uploadToWebhook, just re-log for schedule context
118-
log.error`Schedule "${schedule.name}" webhook failed: ${(err as Error).message}`
164+
const errorMessage = (err as Error).message
165+
log.error`Schedule "${schedule.name}" webhook failed: ${errorMessage}`
166+
167+
// Extract status code from error message if present (e.g., "HTTP 404: Not Found")
168+
const statusMatch = errorMessage.match(/HTTP (\d+):/)
169+
const statusCode = statusMatch?.[1]
170+
? parseInt(statusMatch[1], 10)
171+
: undefined
172+
173+
return {
174+
attempted: true,
175+
success: false,
176+
statusCode,
177+
error: errorMessage,
178+
url: webhookUrl,
179+
}
119180
}
120181
}
121182

trmnl-ha/ha-trmnl/types/domain.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,14 +207,30 @@ export type ScheduleInput = Omit<Schedule, 'id' | 'createdAt' | 'updatedAt'>
207207
/** Schedule data for updates (all fields optional except id) */
208208
export type ScheduleUpdate = Partial<Omit<Schedule, 'id'>>
209209

210+
/** Webhook execution result details */
211+
export interface WebhookResult {
212+
/** Whether webhook was attempted */
213+
attempted: boolean
214+
/** Whether webhook succeeded */
215+
success: boolean
216+
/** HTTP status code from webhook (if attempted) */
217+
statusCode?: number
218+
/** Error message (if failed) */
219+
error?: string
220+
/** Webhook URL that was called */
221+
url?: string
222+
}
223+
210224
/** Response from triggering immediate schedule execution */
211225
export interface SendScheduleResponse {
212-
/** Whether the execution was successful */
226+
/** Whether the overall execution was successful (screenshot captured) */
213227
success: boolean
214228
/** Path where screenshot was saved (if successful) */
215229
savedPath?: string
216-
/** Error message (if failed) */
230+
/** Error message (if screenshot capture failed) */
217231
error?: string
232+
/** Webhook execution details (if webhook was configured) */
233+
webhook?: WebhookResult
218234
}
219235

220236
// =============================================================================

0 commit comments

Comments
 (0)