diff --git a/.changeset/current-price-interval.md b/.changeset/current-price-interval.md new file mode 100644 index 0000000..ac8ca9d --- /dev/null +++ b/.changeset/current-price-interval.md @@ -0,0 +1,5 @@ +--- +"ftw-webapp": patch +--- + +Show the price for the current interval. Respect quarter-hour prices and leave NOW unavailable when viewing tomorrow or a gap in prices. diff --git a/src/vendor/ftw/digests.json b/src/vendor/ftw/digests.json index db4bf8c..e244a1d 100644 --- a/src/vendor/ftw/digests.json +++ b/src/vendor/ftw/digests.json @@ -3,7 +3,7 @@ "ftw-bar-chart.js": "08b61533ed52b0031a39e723278c812b2b27356c1d574b65dfd5603992065d82", "ftw-element.js": "7241b586582187cef2d76d3ee11bbc596c4f428cea3a5c8c84227e113aa3c1a0", "ftw-energy-flow.js": "b6dec1cddebf19da4beed8a1af1b2addc4ed18243b42ed786756696ea060000c", - "ftw-price-chart.js": "ffef7c84af46c5912263fe1275a9af5eca68cee0c08bee8b0bb07727c2de75f4", + "ftw-price-chart.js": "29d32b1f309f58d8431a0a8c2e93bfb67d3b6ea0fc86965a57ddb54ca4e16537", "price-math.js": "bc6b104259f76d799c0a9d7f1d8a780d897cb7dc0d7a3e16a6572fb9c626d508", "price-strip.js": "413b2946c311e910c9a052a8407a3a403ed6caca0e210307c6f3e951896c2b9b", "price-summary.js": "d84cafce59e0cd44939522ba59c0c89979d54cfb414b2c4431a96230e611cd33", diff --git a/src/vendor/ftw/ftw-price-chart.js b/src/vendor/ftw/ftw-price-chart.js index e94a295..cc8fd0c 100644 --- a/src/vendor/ftw/ftw-price-chart.js +++ b/src/vendor/ftw/ftw-price-chart.js @@ -1,4 +1,4 @@ -// Vendored from srcfl/ftw web/components/ftw-price-chart.js at f55eaa2d. +// Vendored from srcfl/ftw web/components/ftw-price-chart.js at fbd29b50781fbd7c11dd297cd3e9a3845c5b746f. // Do not edit here — change it upstream and re-copy. The app and the // box's own dashboard render this exact file; that is the point. // — full-width bar chart of known electricity prices @@ -37,6 +37,7 @@ import { FtwElement } from "./ftw-element.js"; import { apiFetch } from "./api-fetch.js"; import { buildCompactPriceView, + buildPriceSummary, formatPriceSlotLabel, } from "./price-summary.js"; import { bestBlock, consumerTotalOre, priceParts } from "./price-math.js"; @@ -640,31 +641,17 @@ class FtwPriceChart extends FtwElement { } // Compute stats over the visible window using the resolved öre/kWh for // whichever toggle is active, so the numbers in the subtitle line up - // exactly with what the chart bars are showing. `current` is the slot - // covering wall-clock - // now if it's in the window, else the nearest. Empty horizon → no - // stats row, falls through to the existing "no data" message. + // exactly with what the chart bars are showing. Only an interval that + // contains now has a current price. A future window or gap has none. let statsHtml = ""; if (visible.length > 0) { const prices = visible.map(it => this._priceFor(it)); - const now = Date.now(); - let curIdx = visible.findIndex(it => { - const start = (it.tsMs || 0); - const end = start + 60 * 60 * 1000; - return now >= start && now < end; + const { current } = buildPriceSummary(visible, { + totalOn: this._totalOn, + gridTariffOre: this._gridTariff, + vatPercent: this._vatPct, }); - if (curIdx < 0) { - // Nearest by absolute time delta (used when "Tomorrow" tab is - // active and the wall clock is still in today, etc.). - let best = -1, bestD = Infinity; - for (let i = 0; i < visible.length; i++) { - const start = new Date(visible[i].starts_at || visible[i].ts || 0).getTime(); - const d = Math.abs(start - now); - if (d < bestD) { bestD = d; best = i; } - } - curIdx = best; - } - const cur = curIdx >= 0 ? prices[curIdx] : null; + const cur = current ? current.ore : null; const lo = Math.min(...prices); const hi = Math.max(...prices); const avg = prices.reduce((a, b) => a + b, 0) / prices.length; diff --git a/tests/price-chart-fed.test.ts b/tests/price-chart-fed.test.ts index cec3be7..1a34421 100644 --- a/tests/price-chart-fed.test.ts +++ b/tests/price-chart-fed.test.ts @@ -54,6 +54,8 @@ describe('the price chart, fed from the wire', () => { afterEach(() => { document.body.replaceChildren() vi.restoreAllMocks() + vi.useRealTimers() + globalThis.localStorage?.clear() }) it('asks no origin for anything', async () => { @@ -109,4 +111,38 @@ describe('the price chart, fed from the wire', () => { expect(el.shadowRoot!.querySelector('.empty')).not.toBeNull() }) + + it('shows the current quarter from the wire at the interval boundary', async () => { + const now = todayAt(18) + 15 * 60_000 + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(now) + const el = await mount() + el.setPrices(chartPrices({ ...WIRE, slots: [ + { startMs: now - 15 * 60_000, durationMs: 15 * 60_000, spotMinor: 17, totalMinor: 109 }, + { startMs: now, durationMs: 15 * 60_000, spotMinor: 40, totalMinor: 137 }, + ] })) + + const current = el.shadowRoot!.querySelector('.meta-stats > span')!.textContent! + expect(current).toMatch(/now\s+137\.0 öre/) + expect(fetched).not.toHaveBeenCalled() + }) + + it('leaves NOW unavailable when the user selects Tomorrow', async () => { + const now = todayAt(18) + const tomorrow = new Date(now) + tomorrow.setDate(tomorrow.getDate() + 1) + tomorrow.setHours(0, 0, 0, 0) + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(now) + const el = await mount() + el.setPrices(chartPrices({ ...WIRE, slots: [ + { startMs: now, durationMs: 15 * 60_000, spotMinor: 40, totalMinor: 137 }, + { startMs: tomorrow.getTime(), durationMs: 15 * 60_000, spotMinor: 17, totalMinor: 109 }, + ] })) + el.shadowRoot!.querySelector('button[data-horizon="tomorrow"]')!.click() + + const current = el.shadowRoot!.querySelector('.meta-stats > span')!.textContent! + expect(current).toMatch(/now\s+—/) + expect(fetched).not.toHaveBeenCalled() + }) })