This commit is contained in:
Home Assistant Version Control committed 2026-08-31 14:54:46 +00:00
1 parent 38964507bd
commit 1770e33ddc
28 files changed
+692 -232

No files matched your search

@@ -19,6 +19,13 @@ const css = LitElement.prototype.css;
const tmSafePhotoUrl = (u) =>
typeof u === "string" && u.startsWith("/api/taskmate/photo/") ? u : "";
// A pending-claims attribute is only ever usable as a list. The resolver used
// to hand back the pending-approvals sensor's scalar COUNT under this name,
// which turned .filter()/.some() into a TypeError and blanked the whole card
// (#834). The name collision is fixed in the resolver; this keeps a stray
// value from taking the card down again.
const tmClaimList = (v) => (Array.isArray(v) ? v : []);
const _safeColor = (c, d) => (typeof c === "string" && /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : d);
class TaskMateParentDashboardCard extends LitElement {
@@ -29,6 +36,7 @@ class TaskMateParentDashboardCard extends LitElement {
_loading: { type: Object },
_activeSection: { type: String },
_expanded: { type: Object },
_signed: { state: true },
};
}
@@ -69,6 +77,58 @@ class TaskMateParentDashboardCard extends LitElement {
this._loading = {};
this._activeSection = "overview";
this._expanded = {};
this._signed = {}; // safe photo path -> per-viewer signed path
this._inflight = new Set();
}
// Evidence photos come from an auth-gated view; an <img> sends no bearer
// token, so sign each path per-viewer via auth/sign_path. The sensor now
// publishes bare paths, keeping any self-authenticating URL out of shared
// state and binding photo access to the actual viewer.
_photoHref(raw) {
const safe = tmSafePhotoUrl(raw);
return (safe && this._signed[safe]) || "";
}
_collectPhotoUrls() {
const out = [];
const st = this.hass && this.hass.states;
if (!st) return out;
for (const eid in st) {
if (eid.indexOf("sensor.taskmate") !== 0) continue;
const attrs = (st[eid] && st[eid].attributes) || {};
for (const k in attrs) {
const v = attrs[k];
if (!Array.isArray(v)) continue;
for (const item of v) {
if (item && typeof item === "object" && typeof item.photo_url === "string" && item.photo_url) {
out.push(item.photo_url);
}
}
}
}
return out;
}
async _ensureSignedPhotos(rawUrls) {
for (const raw of rawUrls) {
const url = tmSafePhotoUrl(raw);
if (!url || this._signed[url] || this._inflight.has(url)) continue;
this._inflight.add(url);
try {
const res = await this.hass.callWS({ type: "auth/sign_path", path: url, expires: 3600 });
if (res && res.path) this._signed = { ...this._signed, [url]: res.path };
} catch (e) {
console.warn("taskmate: sign_path failed", e);
} finally {
this._inflight.delete(url);
}
}
}
updated() {
const urls = this._collectPhotoUrls();
if (urls.length) this._ensureSignedPhotos(urls);
}
static get styles() {
@@ -527,7 +587,7 @@ class TaskMateParentDashboardCard extends LitElement {
// approve button after a recurring chore resets. `completions` stays
// today-only for the overview tab's activity display.
const pendingCompletions = attrs.chore_completions || completions.filter(c => !c.approved);
const pendingRewardClaims = attrs.pending_reward_claims || [];
const pendingRewardClaims = tmClaimList(attrs.pending_reward_claims);
const pointsIcon = attrs.points_icon || "mdi:star";
const pointsName = attrs.points_name || this._t('common.points');
const totalPending = pendingCompletions.length + pendingRewardClaims.length;
@@ -662,7 +722,7 @@ class TaskMateParentDashboardCard extends LitElement {
const chores = attrs.chores || [];
const completions = attrs.todays_completions || [];
const pendingCompletions = attrs.chore_completions || completions.filter(c => !c.approved);
const pendingRewardClaims = attrs.pending_reward_claims || [];
const pendingRewardClaims = tmClaimList(attrs.pending_reward_claims);
const pointsIcon = attrs.points_icon || "mdi:star";
const pointsName = attrs.points_name || this._t('common.points');
const totalPending = pendingCompletions.length + pendingRewardClaims.length;
@@ -816,7 +876,7 @@ class TaskMateParentDashboardCard extends LitElement {
const time = comp.completed_at
? new Date(comp.completed_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
const pts = comp.points || choreMap[comp.chore_id]?.points || 0;
const photoUrl = tmSafePhotoUrl(comp.photo_url);
const photoUrl = this._photoHref(comp.photo_url);
const photoCap = this._photoCaption(comp.chore_name, child && child.name, comp.completed_at);
const photo = photoUrl
? html`<a class="pd-photo-link" href="${photoUrl}" target="_blank" rel="noopener"
@@ -1149,11 +1209,11 @@ class TaskMateParentDashboardCard extends LitElement {
+${comp.points || chore?.points || 0}
</span>
</div>
${tmSafePhotoUrl(comp.photo_url) ? html`
<a class="approval-photo" href="${tmSafePhotoUrl(comp.photo_url)}" target="_blank" rel="noopener"
@click="${(e) => this._openPhoto(e, tmSafePhotoUrl(comp.photo_url), this._photoCaption(comp.chore_name || chore?.name, child && child.name, comp.completed_at))}"
${this._photoHref(comp.photo_url) ? html`
<a class="approval-photo" href="${this._photoHref(comp.photo_url)}" target="_blank" rel="noopener"
@click="${(e) => this._openPhoto(e, this._photoHref(comp.photo_url), this._photoCaption(comp.chore_name || chore?.name, child && child.name, comp.completed_at))}"
title="${this._t('approvals.view_photo') || 'View photo'}">
<img src="${tmSafePhotoUrl(comp.photo_url)}" alt="" loading="lazy">
<img src="${this._photoHref(comp.photo_url)}" alt="" loading="lazy">
</a>
` : ''}
</div>