155 files
This commit is contained in:
1 parent
a212615470
commit
0ff8c287a8
155 files changed
+19264
-3148
No files matched your search
@@ -1,6 +1,7 @@
|
||||
/** Shared chart helpers: nice axis ticks and consistent number/date formats. */
|
||||
|
||||
import { formatDateShort, formatTimeOfDay, formatNumber } from "../styles";
|
||||
import { displayTimeZone, ymdIn } from "../helpers/ha-time";
|
||||
|
||||
/** SVG / CSS coordinate: one decimal, ALWAYS a dot — never a user-facing
|
||||
* number. The only place outside styles.ts allowed to call toFixed (tripwire
|
||||
@@ -75,9 +76,12 @@ export function fmtDateTime(ts: number, lang: string): string {
|
||||
}
|
||||
|
||||
/** Whether date ticks need the year: the range crosses a calendar-year
|
||||
* boundary (a "Jun … Feb" sequence would otherwise read backwards). */
|
||||
* boundary (a "Jun … Feb" sequence would otherwise read backwards) — in
|
||||
* the zone the ticks are shown in. */
|
||||
export function needsYear(tsMin: number, tsMax: number): boolean {
|
||||
return new Date(tsMin).getFullYear() !== new Date(tsMax).getFullYear();
|
||||
if (!Number.isFinite(tsMin) || !Number.isFinite(tsMax)) return false;
|
||||
const zone = displayTimeZone();
|
||||
return ymdIn(tsMin, zone).slice(0, 4) !== ymdIn(tsMax, zone).slice(0, 4);
|
||||
}
|
||||
|
||||
/** Evenly-spaced x-axis tick timestamps (first/last inclusive). */
|
||||
|
||||
@@ -11,6 +11,7 @@ import { t, formatCost } from "../styles";
|
||||
import { niceTicks, fmtNum, fmtDateTick, timeTicks, needsYear, px } from "./chart-utils";
|
||||
import type { MaintenanceTask } from "../types";
|
||||
import { entrySpend } from "../helpers/parts-cost";
|
||||
import { stampMs } from "../helpers/ha-time";
|
||||
|
||||
const COST_CHART_H = 200;
|
||||
const PAD_T = 10;
|
||||
@@ -63,7 +64,7 @@ export function renderCostDurationCard(
|
||||
function renderHistoryChart(task: MaintenanceTask, lang: string, toggle: "cost" | "duration" | "both", currencySymbol: string) {
|
||||
const entries = task.history
|
||||
.filter((h) => h.type === "completed" && (entrySpend(h) != null || h.duration != null))
|
||||
.map((h) => ({ ts: new Date(h.timestamp).getTime(), cost: entrySpend(h) ?? 0, duration: h.duration ?? 0 }))
|
||||
.map((h) => ({ ts: stampMs(h.timestamp), cost: entrySpend(h) ?? 0, duration: h.duration ?? 0 }))
|
||||
.sort((a, b) => a.ts - b.ts);
|
||||
|
||||
if (entries.length < 2) return nothing;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { renderRefChip } from "../helpers/reference";
|
||||
import { entryReadingValues } from "../helpers/reading-slots";
|
||||
import { matchesQuery } from "../helpers/search-match";
|
||||
import { historyNoteText } from "../helpers/history-note";
|
||||
import { stampMs } from "../helpers/ha-time";
|
||||
|
||||
/** v2.2.0: only "lifecycle + cost-bearing" entries are user-editable.
|
||||
* Triggers are auto-generated by sensors and shouldn't be retroactively
|
||||
@@ -34,9 +35,9 @@ export const EDITABLE_HISTORY_TYPES: readonly HistoryEntryType[] = ["completed",
|
||||
* an unparsable) timestamp keep "later appended = newer". */
|
||||
export function newestFirst<T extends { timestamp?: string | null }>(history: readonly T[] | null | undefined): T[] {
|
||||
const ms = (e: T): number => {
|
||||
const raw = e.timestamp ?? "";
|
||||
// Date-only strings are local days (like formatDate) — not UTC midnight.
|
||||
const v = Date.parse(raw.length === 10 ? `${raw}T00:00:00` : raw);
|
||||
// A zone-less stamp (the history editor writes one) is HA local time,
|
||||
// a date-only one HA's midnight — never UTC midnight.
|
||||
const v = stampMs(e.timestamp);
|
||||
return Number.isNaN(v) ? -Infinity : v;
|
||||
};
|
||||
return (history ?? [])
|
||||
|
||||
@@ -15,6 +15,7 @@ import { html, nothing } from "lit";
|
||||
import { t, formatDate, formatDueDays, formatNumber } from "../styles";
|
||||
import { px } from "./chart-utils";
|
||||
import { daysProgress } from "../helpers/interval";
|
||||
import { stampMs } from "../helpers/ha-time";
|
||||
import type { MaintenanceTask, TaskRow, StatisticsPoint } from "../types";
|
||||
|
||||
const MINI_SPARKLINE_W = 60;
|
||||
@@ -61,7 +62,7 @@ export function computeTrend(
|
||||
? []
|
||||
: (row.history || [])
|
||||
.filter((h) => h.trigger_value != null)
|
||||
.map((h) => ({ ts: new Date(h.timestamp).getTime(), val: h.trigger_value as number }));
|
||||
.map((h) => ({ ts: stampMs(h.timestamp), val: h.trigger_value as number }));
|
||||
if (row.trigger_current_value != null) points = [...points, { ts: Date.now(), val: row.trigger_current_value }];
|
||||
if (points.length < 2) return null;
|
||||
points.sort((a, b) => a.ts - b.ts);
|
||||
@@ -249,7 +250,7 @@ export function renderMiniSparkline(
|
||||
if (!row.history) return nothing;
|
||||
for (const h of row.history) {
|
||||
if (h.trigger_value != null) {
|
||||
points.push({ ts: new Date(h.timestamp).getTime(), val: h.trigger_value });
|
||||
points.push({ ts: stampMs(h.timestamp), val: h.trigger_value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { html, svg, nothing } from "lit";
|
||||
import { t, formatNumber } from "../styles";
|
||||
import { px } from "./chart-utils";
|
||||
import { haToday } from "../helpers/ha-time";
|
||||
import type { MaintenanceTask, AdvancedFeatures } from "../types";
|
||||
|
||||
const MONTH_KEYS = [
|
||||
@@ -11,13 +12,18 @@ const MONTH_KEYS = [
|
||||
"month_sep", "month_oct", "month_nov", "month_dec",
|
||||
];
|
||||
|
||||
/** HA's current month (0 = Jan) — the one the backend applies the factor of. */
|
||||
function haMonth(): number {
|
||||
return Number(haToday().slice(5, 7)) - 1;
|
||||
}
|
||||
|
||||
export function renderSeasonalCardCompact(task: MaintenanceTask, lang: string, features: AdvancedFeatures) {
|
||||
if (!features.seasonal || !task.seasonal_factor || task.seasonal_factor === 1.0) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const months = MONTH_KEYS.map(k => t(k, lang));
|
||||
const currentMonth = new Date().getMonth();
|
||||
const currentMonth = haMonth();
|
||||
|
||||
const realFactors = task.seasonal_factors || task.interval_analysis?.seasonal_factors || null;
|
||||
const seasonalData = realFactors && realFactors.length === 12
|
||||
@@ -66,7 +72,7 @@ function renderSeasonalChart(task: MaintenanceTask, lang: string) {
|
||||
if (!factors || factors.length !== 12) return nothing;
|
||||
|
||||
const reason = task.interval_analysis?.seasonal_reason;
|
||||
const currentMonth = new Date().getMonth();
|
||||
const currentMonth = haMonth();
|
||||
const W = 300, H = 100;
|
||||
const PAD_T = 8, PAD_B = 4;
|
||||
const chartH = H - PAD_T - PAD_B;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { html, nothing } from "lit";
|
||||
import { t, fireMoreInfo, formatDueDays } from "../styles";
|
||||
import { fmtNum, fmtVal } from "./chart-utils";
|
||||
import { dueDaysOf } from "./progress";
|
||||
import { stampMs } from "../helpers/ha-time";
|
||||
import "../components/trigger-chart";
|
||||
import type { ChartPoint, ChartEvent } from "../components/trigger-chart";
|
||||
import type { MaintenanceTask, TriggerConfig, StatisticsPoint } from "../types";
|
||||
@@ -299,8 +300,8 @@ function counterBaseline(task: MaintenanceTask, rawPoints: ChartPoint[]): { valu
|
||||
function lastServiceTs(task: MaintenanceTask): number | null {
|
||||
const e = [...task.history]
|
||||
.filter((h) => h.type === "completed" || h.type === "reset")
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())[0];
|
||||
return e ? new Date(e.timestamp).getTime() : null;
|
||||
.sort((a, b) => stampMs(b.timestamp) - stampMs(a.timestamp))[0];
|
||||
return e ? stampMs(e.timestamp) : null;
|
||||
}
|
||||
|
||||
/** "8,507 / 15,000 km · 57 %" header + progress bar (counter / state_change / runtime). */
|
||||
@@ -351,7 +352,7 @@ function rawStatsPoints(task: MaintenanceTask, ctx: SparklineContext): ChartPoin
|
||||
} else {
|
||||
for (const h of task.history) {
|
||||
if (h.trigger_value != null) {
|
||||
points.push({ ts: new Date(h.timestamp).getTime(), val: h.trigger_value });
|
||||
points.push({ ts: stampMs(h.timestamp), val: h.trigger_value });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -472,7 +473,7 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
|
||||
|
||||
const events: ChartEvent[] = task.history
|
||||
.filter((h) => ["completed", "skipped", "reset"].includes(h.type))
|
||||
.map((h) => ({ ts: new Date(h.timestamp).getTime(), type: h.type }));
|
||||
.map((h) => ({ ts: stampMs(h.timestamp), type: h.type }));
|
||||
|
||||
return html`
|
||||
<maintenance-trigger-chart
|
||||
|
||||
Reference in new issue
Block a user