28 files
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user