/** Dialog for generating, printing, and downloading QR codes. * * For tasks: shows two QR codes side-by-side — "Info" (ℹ) and "Complete" (✓) * with embedded icons. For objects: shows a single "Info" QR. */ import { LitElement, html, css, nothing } from "lit"; import { property, state } from "lit/decorators.js"; import type { HomeAssistant } from "../types"; import { t } from "../styles"; interface QrResult { svg_data_uri: string; url: string; label: { object_name: string; manufacturer: string; model: string; task_name: string | null; }; } /** Escape HTML entities to prevent XSS in document.write contexts. */ function escapeHtml(s: string): string { return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } /** Sanitize a data URI for safe use in an img src attribute. */ function sanitizeDataUri(uri: string): string { if (!uri.startsWith("data:image/svg+xml,") && !uri.startsWith("data:image/png;base64,")) { return ""; } return escapeHtml(uri); } /** Sanitize a string for use in a filename (remove OS-invalid chars). */ function sanitizeFilename(s: string): string { return s.replace(/[/\\:*?"<>|#%]+/g, "").replace(/\s+/g, "-").toLowerCase().substring(0, 100); } export class MaintenanceQrDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() public lang = "en"; @state() private _open = false; @state() private _loading = false; @state() private _error = ""; @state() private _viewResult: QrResult | null = null; @state() private _completeResult: QrResult | null = null; @state() private _urlMode: "companion" | "local" | "server" = "companion"; private _entryId = ""; private _taskId: string | null = null; private _objectName = ""; private _taskName = ""; private _generateSeq = 0; public openForObject(entryId: string, objectName: string): void { this._entryId = entryId; this._taskId = null; this._objectName = objectName; this._taskName = ""; this._urlMode = "companion"; this._error = ""; this._viewResult = null; this._completeResult = null; this._open = true; this._generate(); } public openForTask( entryId: string, taskId: string, objectName: string, taskName: string, ): void { this._entryId = entryId; this._taskId = taskId; this._objectName = objectName; this._taskName = taskName; this._urlMode = "companion"; this._error = ""; this._viewResult = null; this._completeResult = null; this._open = true; this._generate(); } private async _generate(): Promise { const seq = ++this._generateSeq; this._loading = true; this._error = ""; this._viewResult = null; this._completeResult = null; try { const base: Record = { type: "maintenance_supporter/qr/generate", entry_id: this._entryId, url_mode: this._urlMode, }; if (this._taskId) base.task_id = this._taskId; // Always request "view" QR; also request "complete" if this is a task const promises: Promise[] = [ this.hass.connection.sendMessagePromise({ ...base, action: "view" }), ]; if (this._taskId) { promises.push( this.hass.connection.sendMessagePromise({ ...base, action: "complete" }), ); } const results = await Promise.all(promises); if (seq !== this._generateSeq) return; this._viewResult = results[0] as QrResult; if (results.length > 1) { this._completeResult = results[1] as QrResult; } } catch (err: unknown) { if (seq !== this._generateSeq) return; const code = (err as Record)?.code; const msg = (err as Record)?.message; this._error = code === "no_url" || (typeof msg === "string" && msg.includes("No Home Assistant URL")) ? t("qr_error_no_url", this.lang) : t("qr_error", this.lang); } finally { if (seq === this._generateSeq) this._loading = false; } } private _setUrlMode(mode: "companion" | "local" | "server"): void { if (this._urlMode === mode) return; this._urlMode = mode; this._generate(); } private _print(): void { if (!this._viewResult) return; const r = this._viewResult; const title = r.label.task_name ? `${r.label.object_name} — ${r.label.task_name}` : r.label.object_name; const subtitle = [r.label.manufacturer, r.label.model] .filter(Boolean) .join(" "); const w = window.open("", "_blank", "width=600,height=500"); if (!w) return; const L = this.lang || "en"; const safeTitle = escapeHtml(title); const safeSub = escapeHtml(subtitle); const hasComplete = !!this._completeResult; const viewLabel = escapeHtml(t("qr_action_view", L)); const completeLabel = escapeHtml(t("qr_action_complete", L)); w.document.write(` ${safeTitle}

${safeTitle}

${safeSub ? `
${safeSub}
` : ""}
QR Info
${viewLabel}
${hasComplete ? `
QR Complete
${completeLabel}
` : ""}
${escapeHtml(this._viewResult.url)}