Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/current-price-interval.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion src/vendor/ftw/digests.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 9 additions & 22 deletions src/vendor/ftw/ftw-price-chart.js
Original file line number Diff line number Diff line change
@@ -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.
// <ftw-price-chart> — full-width bar chart of known electricity prices
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down
36 changes: 36 additions & 0 deletions tests/price-chart-fed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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<HTMLButtonElement>('button[data-horizon="tomorrow"]')!.click()

const current = el.shadowRoot!.querySelector('.meta-stats > span')!.textContent!
expect(current).toMatch(/now\s+—/)
expect(fetched).not.toHaveBeenCalled()
})
})