Skip to content

Commit a88c695

Browse files
ikraamgclaude
andcommitted
Fixed Fetch URL to use server port instead of ingress origin
When accessed via Home Assistant's add-on panel, window.location.origin returns the HA ingress URL which isn't reachable externally. The Fetch URL now constructs the full URL using hostname + configured server port (injected via uiConfig), making it correct for pull-mode integrations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3237410 commit a88c695

3 files changed

Lines changed: 25 additions & 17 deletions

File tree

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -680,18 +680,17 @@ class App {
680680
// =============================================================================
681681

682682
/**
683-
* Copies the fetch URL (with full origin) to clipboard.
683+
* Copies the fetch URL to clipboard.
684+
* NOTE: The input already contains the full URL (with hostname:port)
684685
*/
685686
async copyFetchUrl(): Promise<void> {
686687
const input = document.getElementById(
687688
's_fetch_url',
688689
) as HTMLInputElement | null
689690
if (!input) return
690691

691-
const fullUrl = `${window.location.origin}${input.value}`
692-
693692
try {
694-
await navigator.clipboard.writeText(fullUrl)
693+
await navigator.clipboard.writeText(input.value)
695694
// Brief visual feedback
696695
const copyBtn = input.nextElementSibling as HTMLButtonElement | null
697696
if (copyBtn) {
@@ -703,7 +702,6 @@ class App {
703702
}
704703
} catch {
705704
// Fallback: select the text for manual copy
706-
input.value = fullUrl
707705
input.select()
708706
}
709707
}

trmnl-ha/ha-trmnl/html/js/ui-renderer.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,19 @@ export class RenderScheduleContent {
230230
const queryString = params.toString()
231231
const fetchPath = queryString ? `${path}?${queryString}` : path
232232

233+
// Build full URL using hostname + server port (not window.location.origin which
234+
// would be the HA ingress URL when accessed via HA's add-on panel)
235+
// @ts-expect-error window.uiConfig is injected by server
236+
const uiConfig = window.uiConfig || { serverPort: 10000 }
237+
const baseUrl = `http://${window.location.hostname}:${uiConfig.serverPort}`
238+
const fullFetchUrl = `${baseUrl}${fetchPath}`
239+
233240
return `
234241
<div class="mt-3 p-3 rounded-md" style="background-color: #f0f9ff; border: 1px solid #bae6fd">
235242
<label class="block text-sm font-medium text-gray-700 mb-1">Fetch URL</label>
236243
<div class="flex gap-2">
237244
<input type="text" id="s_fetch_url" readonly
238-
value="${fetchPath}"
245+
value="${fullFetchUrl}"
239246
class="w-full px-3 py-2 border rounded-md bg-white font-mono text-xs"
240247
style="border-color: #93c5fd"
241248
title="GET this URL to receive a screenshot with the current settings" />

trmnl-ha/ha-trmnl/ui.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import type { HaWebSocket as LibHaWebSocket } from 'home-assistant-js-websocket/
2525
import * as messages from 'home-assistant-js-websocket/dist/messages.js'
2626
import { atLeastHaVersion } from 'home-assistant-js-websocket/dist/util.js'
2727
import WebSocket from 'ws'
28-
import { hassUrl, hassToken } from './const.js'
28+
import { hassUrl, hassToken, SERVER_PORT } from './const.js'
2929
import { loadPresets } from './devices.js'
3030
import type { PresetsConfig } from './types/domain.js'
3131
import { uiLogger } from './lib/logger.js'
@@ -85,6 +85,8 @@ interface UIConfig {
8585
connectionStatus: string
8686
/** Timestamp when HA data was last fetched (for cache age display) */
8787
cachedAt: number | null
88+
/** Server port for constructing Fetch URLs (10000 for add-on, configurable for standalone) */
89+
serverPort: number
8890
}
8991

9092
// =============================================================================
@@ -134,7 +136,7 @@ function extractHostname(url: string): string | null {
134136
* Returns detailed diagnostic info for error logging
135137
*/
136138
async function checkDnsResolution(
137-
hostname: string
139+
hostname: string,
138140
): Promise<{ resolved: boolean; ip?: string; error?: string }> {
139141
try {
140142
const result = await lookup(hostname)
@@ -160,7 +162,7 @@ async function checkDnsResolution(
160162
* than the browser's WebSocket, but they're compatible enough for our use case.
161163
*/
162164
function createSocketWithSslBypass(
163-
options: ConnectionOptions
165+
options: ConnectionOptions,
164166
): Promise<LibHaWebSocket> {
165167
if (!options.auth) {
166168
throw new Error('Auth is required for WebSocket connection')
@@ -177,7 +179,7 @@ function createSocketWithSslBypass(
177179
function connect(
178180
triesLeft: number,
179181
promResolve: (socket: LibHaWebSocket) => void,
180-
promReject: (err: unknown) => void
182+
promReject: (err: unknown) => void,
181183
) {
182184
log.debug`WebSocket connection attempt (retries left: ${triesLeft})`
183185

@@ -193,7 +195,7 @@ function createSocketWithSslBypass(
193195
let invalidAuth = false
194196

195197
const closeMessage = (
196-
event?: WebSocket.CloseEvent | WebSocket.ErrorEvent
198+
event?: WebSocket.CloseEvent | WebSocket.ErrorEvent,
197199
) => {
198200
socket.removeEventListener('close', closeMessage)
199201

@@ -306,7 +308,7 @@ function createSocketWithSslBypass(
306308
function sendHtmlResponse(
307309
response: ServerResponse,
308310
html: string,
309-
statusCode: number = 200
311+
statusCode: number = 200,
310312
): void {
311313
response.writeHead(statusCode, {
312314
'Content-Type': 'text/html',
@@ -328,12 +330,12 @@ const HA_CONNECTION_TIMEOUT = 5000
328330
function withTimeout<T>(
329331
promise: Promise<T>,
330332
ms: number,
331-
message: string
333+
message: string,
332334
): Promise<T> {
333335
return Promise.race([
334336
promise,
335337
new Promise<T>((_, reject) =>
336-
setTimeout(() => reject(new Error(message)), ms)
338+
setTimeout(() => reject(new Error(message)), ms),
337339
),
338340
])
339341
}
@@ -399,7 +401,7 @@ async function fetchHomeAssistantData(): Promise<HomeAssistantData> {
399401
createSocket: createSocketWithSslBypass,
400402
}),
401403
HA_CONNECTION_TIMEOUT,
402-
`HA connection timeout after ${HA_CONNECTION_TIMEOUT}ms`
404+
`HA connection timeout after ${HA_CONNECTION_TIMEOUT}ms`,
403405
)
404406

405407
log.debug`WebSocket connected, fetching HA data...`
@@ -491,7 +493,7 @@ async function getCachedOrFetch(forceRefresh: boolean): Promise<{
491493
// Return cached data if available and not forcing refresh
492494
if (cachedHassData && !forceRefresh) {
493495
log.debug`Using cached HA data (age: ${Math.round(
494-
(Date.now() - cacheTimestamp) / 1000
496+
(Date.now() - cacheTimestamp) / 1000,
495497
)}s)`
496498
return { data: cachedHassData, cachedAt: cacheTimestamp }
497499
}
@@ -520,7 +522,7 @@ async function getCachedOrFetch(forceRefresh: boolean): Promise<{
520522
*/
521523
export async function handleUIRequest(
522524
response: ServerResponse,
523-
requestUrl?: URL
525+
requestUrl?: URL,
524526
): Promise<void> {
525527
try {
526528
// Check for forced refresh via query param
@@ -581,6 +583,7 @@ export async function handleUIRequest(
581583
tokenPreview,
582584
connectionStatus,
583585
cachedAt,
586+
serverPort: SERVER_PORT,
584587
}
585588

586589
const htmlPath = join(HTML_DIR, 'index.html')

0 commit comments

Comments
 (0)