feat(frontend): Odśwież button beside every screen title, with its text
CI / Check (gateway, backend, frontend) (push) Successful in 57s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-24 19:08:15 +02:00
1 parent 87d111398c
commit 64ffd035c5
5 files changed
+22 -12

No files matched your search

+1 -1
View File
@@ -20,7 +20,7 @@ and gateway's versions in the "Wersje" card of the Stan screen.
The Energia screen has three views, kept in the URL hash so Back works: `#energy` (Teraz), `#energy/battery` (Bateria, also
opened by tapping the battery in the flow diagram) and `#energy/inverters` (Falowniki, the device details).
Every screen starts with the "Odśwież" button (`Refresh.tsx`): it posts `api/refresh`, waits 5 s for the gateway's extra poll, then
Every screen title (`PageTitle`) has the "Odśwież" button (`Refresh.tsx`) beside it, with its status text below: it posts `api/refresh`, waits 5 s for the gateway's extra poll, then
`refreshAll()` (`poll.ts`) fetches every poller that is on screen again. When the gateway skipped every device (polled within a minute) it says so and
waits out the shortest cooldown.
-2
View File
@@ -3,7 +3,6 @@ import { celsius, hour, longDate } from "./format";
import { Icon, type IconName } from "./icons";
import { Energy } from "./Energy";
import { Overview } from "./Overview";
import { RefreshButton } from "./Refresh";
import { usePoll } from "./poll";
import { useTheme, type Theme } from "./theme";
import type { HeatPump } from "./types";
@@ -120,7 +119,6 @@ export function App() {
<div className="hidden flex-1 lg:flex lg:flex-col"><SideFooter /></div>
</nav>
<main className="mx-auto max-w-[1500px] px-4 pt-5 pb-28 sm:px-6 lg:px-8 lg:pt-8 lg:pb-10">
<RefreshButton />
<Suspense fallback={<Loading />}><View /></Suspense>
<ThemeSwitch className="mx-auto mt-8 max-w-xs border border-edge bg-panel text-ink lg:hidden" />
</main>
+11 -1
View File
@@ -4,6 +4,7 @@ import { answer, example, serve } from "./fixtures";
const button = () => screen.getByRole("button", { name: "Odśwież" });
const status = () => screen.getByRole("status");
const noStatus = () => screen.queryByRole("status");
// The button and a poller that shows api/heat-pump, as a screen would.
async function shown(onPost: () => Promise<Response>) {
@@ -68,7 +69,7 @@ describe("RefreshButton", () => {
expect(status()).toHaveTextContent("Dane są świeże – spróbuj za 20 s");
await act(() => vi.advanceTimersByTimeAsync(20_000));
expect(status()).toBeEmptyDOMElement();
expect(noStatus()).toBeNull();
expect(button()).toBeEnabled();
});
@@ -95,4 +96,13 @@ describe("RefreshButton", () => {
expect(status()).toHaveTextContent("Brak połączenia z siecią");
});
it("sits in the heading row of a screen, next to the title", async () => {
vi.stubGlobal("fetch", vi.fn(() => Promise.reject(new TypeError("offline"))));
const { PageTitle } = await import("./ui");
render(<PageTitle title="Stan" />);
expect(screen.getByRole("banner")).toContainElement(button());
expect(button()).toHaveTextContent("Odśwież");
});
});
+5 -5
View File
@@ -56,13 +56,13 @@ export function RefreshButton() {
}[state.kind];
const busy = state.kind === "busy";
return (
<div className="mb-4 flex items-center justify-end gap-3">
<p role="status" className={`text-sm ${state.kind === "error" ? "text-bad" : "text-soft"}`}>{text}</p>
<button onClick={tap} disabled={busy || state.kind === "fresh"} aria-label="Odśwież"
className="flex min-h-11 min-w-11 items-center justify-center gap-2 rounded-xl border-2 border-brand bg-panel px-3 font-bold text-brand transition active:scale-[0.98] disabled:opacity-40">
<div className="flex shrink-0 flex-col items-end gap-1">
<button onClick={tap} disabled={busy || state.kind === "fresh"}
className="flex min-h-11 items-center justify-center gap-2 rounded-xl border-2 border-brand bg-panel px-3 font-bold text-brand transition active:scale-[0.98] disabled:opacity-40">
<Icon name="refresh" className={`size-5 ${busy ? "animate-spin" : ""}`} />
<span className="hidden sm:inline">Odśwież</span>
Odśwież
</button>
{text && <p role="status" className={`max-w-44 text-right text-xs ${state.kind === "error" ? "text-bad" : "text-soft"}`}>{text}</p>}
</div>
);
}
+5 -3
View File
@@ -1,5 +1,6 @@
import type { ReactNode } from "react";
import { ageMinutes, ago } from "./format";
import { RefreshButton } from "./Refresh";
export const STALE_MINUTES = 10;
@@ -103,14 +104,15 @@ export function Row({ label, value, icon }: { label: ReactNode; value: ReactNode
);
}
export function PageTitle({ title, subtitle, aside }: { title: string; subtitle?: ReactNode; aside?: ReactNode }) {
// Every screen starts with this, so the refresh button sits beside each title.
export function PageTitle({ title, subtitle }: { title: string; subtitle?: ReactNode }) {
return (
<header className="mb-5 flex flex-wrap items-end justify-between gap-3">
<header className="mb-5 flex items-center justify-between gap-3">
<div>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">{title}</h1>
{subtitle && <p className="mt-1 text-sm text-soft">{subtitle}</p>}
</div>
{aside}
<RefreshButton />
</header>
);
}