poprawki i zmiany ux

This commit is contained in:
Mateusz Gruszczyński
2026-03-26 09:30:39 +01:00
parent fd0f645251
commit 138059945e
28 changed files with 1000 additions and 225 deletions

View File

@@ -2,15 +2,17 @@ import type { EChartsOption } from "echarts";
import { Card } from "../common/Card";
import { EChart } from "../common/EChart";
import type { BucketPoint } from "../../types";
import { t, type Language } from "../../i18n";
interface ComparisonChartProps {
current: BucketPoint[];
comparison: BucketPoint[];
unit: string;
compareMode: string;
language?: Language;
}
export function ComparisonChart({ current, comparison, unit, compareMode }: ComparisonChartProps) {
export function ComparisonChart({ current, comparison, unit, compareMode, language = "en" }: ComparisonChartProps) {
const option: EChartsOption = {
tooltip: {
trigger: "axis",
@@ -43,14 +45,14 @@ export function ComparisonChart({ current, comparison, unit, compareMode }: Comp
},
series: [
{
name: "Aktualny okres",
name: t(language, "currentPeriod"),
type: "bar",
barGap: "20%",
itemStyle: { color: "#60a5fa", borderRadius: [10, 10, 0, 0] },
data: current.map((item) => item.value),
},
{
name: compareMode === "previous_year" ? "Poprzedni rok" : "Poprzedni okres",
name: compareMode === "previous_year" ? t(language, "comparePreviousYear") : t(language, "comparePreviousPeriod"),
type: "bar",
itemStyle: { color: "#f59e0b", borderRadius: [10, 10, 0, 0] },
data: current.map((_, index) => comparison[index]?.value ?? 0),
@@ -59,7 +61,7 @@ export function ComparisonChart({ current, comparison, unit, compareMode }: Comp
};
return (
<Card title="Porownanie okresow" subtitle="Wspolne slupki dla aktualnego i porownawczego okresu">
<Card title={t(language, "chartComparison")} subtitle={t(language, "chartComparisonSubtitle")}>
<EChart option={option} className="h-[340px] w-full" />
</Card>
);

View File

@@ -1,4 +1,5 @@
import { Card } from "../common/Card";
import { t, type Language } from "../../i18n";
interface PeriodControlsProps {
rangeKey: string;
@@ -10,6 +11,7 @@ interface PeriodControlsProps {
onRangeChange: (value: string) => void;
onBucketChange: (value: string) => void;
onCompareChange: (value: string) => void;
language?: Language;
}
export function PeriodControls({
@@ -22,12 +24,13 @@ export function PeriodControls({
onRangeChange,
onBucketChange,
onCompareChange,
language = "en",
}: PeriodControlsProps) {
return (
<Card title="Porownanie okresow" subtitle="Dzien / tydzien / miesiac + poprzedni okres lub poprzedni rok">
<Card title={t(language, "chartComparison")} subtitle={t(language, "chartComparisonControlSubtitle")}>
<div className="grid gap-4 lg:grid-cols-3">
<label className="space-y-2">
<span className="text-xs uppercase tracking-[0.18em] text-slate-500">Zakres</span>
<span className="text-xs uppercase tracking-[0.18em] text-slate-500">{t(language, "range")}</span>
<select
value={rangeKey}
onChange={(event) => onRangeChange(event.target.value)}
@@ -42,7 +45,7 @@ export function PeriodControls({
</label>
<label className="space-y-2">
<span className="text-xs uppercase tracking-[0.18em] text-slate-500">Bucket</span>
<span className="text-xs uppercase tracking-[0.18em] text-slate-500">{t(language, "bucket")}</span>
<select
value={bucket}
onChange={(event) => onBucketChange(event.target.value)}
@@ -57,7 +60,7 @@ export function PeriodControls({
</label>
<label className="space-y-2">
<span className="text-xs uppercase tracking-[0.18em] text-slate-500">Porownanie</span>
<span className="text-xs uppercase tracking-[0.18em] text-slate-500">{t(language, "comparisonPeriod")}</span>
<select
value={compare}
onChange={(event) => onCompareChange(event.target.value)}

View File

@@ -9,23 +9,41 @@ interface EChartProps {
export function EChart({ option, className = "h-80 w-full" }: EChartProps) {
const ref = useRef<HTMLDivElement | null>(null);
const chartRef = useRef<echarts.EChartsType | null>(null);
useEffect(() => {
if (!ref.current) {
if (!ref.current || chartRef.current) {
return;
}
const chart = echarts.init(ref.current);
chart.setOption(option);
chartRef.current = echarts.init(ref.current);
const observer = new ResizeObserver(() => chart.resize());
return () => {
chartRef.current?.dispose();
chartRef.current = null;
};
}, []);
useEffect(() => {
if (!chartRef.current) {
return;
}
chartRef.current.setOption(option, { notMerge: false, lazyUpdate: true });
}, [option]);
useEffect(() => {
if (!ref.current || !chartRef.current) {
return;
}
const observer = new ResizeObserver(() => chartRef.current?.resize());
observer.observe(ref.current);
return () => {
observer.disconnect();
chart.dispose();
};
}, [option]);
}, []);
return <div ref={ref} className={className} />;
}

View File

@@ -2,13 +2,15 @@ import type { EChartsOption } from "echarts";
import { Card } from "../common/Card";
import { EChart } from "../common/EChart";
import type { HistoryPayload } from "../../types";
import type { Language } from "../../i18n";
interface LiveHistoryChartProps {
history?: HistoryPayload;
title?: string;
language?: Language;
}
export function LiveHistoryChart({ history, title = "Dane chwilowe" }: LiveHistoryChartProps) {
export function LiveHistoryChart({ history, title = "Live data", language = "en" }: LiveHistoryChartProps) {
const option: EChartsOption = {
tooltip: {
trigger: "axis",
@@ -33,7 +35,7 @@ export function LiveHistoryChart({ history, title = "Dane chwilowe" }: LiveHisto
axisLabel: { color: "#94a3b8" },
axisLine: { lineStyle: { color: "rgba(255,255,255,0.08)" } },
data: history?.series[0]?.points.map((point) =>
new Date(point.timestamp).toLocaleTimeString("pl-PL", { hour: "2-digit", minute: "2-digit" })
new Date(point.timestamp).toLocaleTimeString(language === "en" ? "en-GB" : "pl-PL", { hour: "2-digit", minute: "2-digit" })
) ?? [],
},
yAxis: {
@@ -56,7 +58,7 @@ export function LiveHistoryChart({ history, title = "Dane chwilowe" }: LiveHisto
return (
<Card
title={title}
subtitle="Moc AC, moce stringow DC i opcjonalnie temperatura falownika w jednym widoku live"
subtitle={language === "en" ? "AC power, DC string power and optional inverter temperature in one live view" : "Moc AC, moce stringów DC i opcjonalnie temperatura falownika w jednym widoku live"}
>
<EChart option={option} className="h-[340px] w-full" />
</Card>

View File

@@ -1,14 +1,16 @@
import { Card } from "../common/Card";
import { ValuePair } from "../common/ValuePair";
import type { SnapshotGroupRow } from "../../types";
import type { Language } from "../../i18n";
interface PhaseGridProps {
rows: SnapshotGroupRow[];
language?: Language;
}
export function PhaseGrid({ rows }: PhaseGridProps) {
export function PhaseGrid({ rows, language = "en" }: PhaseGridProps) {
return (
<Card title="Fazy AC" subtitle="Napiece, prady i moce pozorne na falowniku">
<Card title={language === "en" ? "AC phases" : "Fazy AC"} subtitle={language === "en" ? "Voltage, current and apparent power on the inverter" : "Napięcie, prądy i moce pozorne na falowniku"}>
<div className="grid gap-4 md:grid-cols-3">
{rows.map((row) => (
<div key={row.id} className="rounded-3xl border border-white/10 bg-slate-950/40 p-4">

View File

@@ -1,16 +1,18 @@
import { Card } from "../common/Card";
import { ValuePair } from "../common/ValuePair";
import type { SnapshotGroupRow } from "../../types";
import type { Language } from "../../i18n";
interface StringGridProps {
rows: SnapshotGroupRow[];
language?: Language;
}
const slotOrder = ["power", "voltage"] as const;
export function StringGrid({ rows }: StringGridProps) {
export function StringGrid({ rows, language = "en" }: StringGridProps) {
return (
<Card title="Stringi DC" subtitle="Widok automatycznie skaluje sie do liczby stringow i dostepnych metryk z config.py">
<Card title={language === "en" ? "DC strings" : "Stringi DC"} subtitle={language === "en" ? "The layout automatically scales to the number of strings and available metrics from config.py" : "Widok automatycznie skaluje się do liczby stringów i dostępnych metryk z config.py"}>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{rows.map((row) => {
const visibleSlots = slotOrder.filter((slot) => row.values[slot]);

View File

@@ -9,24 +9,24 @@ interface ConfigPanelProps {
export function ConfigPanel({ config }: ConfigPanelProps) {
return (
<div className="grid gap-6 xl:grid-cols-[1.1fr_1fr]">
<Card title="Architektura i UX" subtitle="Co zostalo przygotowane w tej wersji">
<Card title="Architecture and UX" subtitle="What is prepared in this version">
<div className="grid gap-4 text-sm text-slate-300">
<div className="rounded-2xl border border-white/10 bg-slate-950/30 p-4">
<div className="font-medium text-white">Backend</div>
<p className="mt-2">Flask + modularne serwisy, bez uvicorna i bez pydantic-core. Odczyt z InfluxDB idzie po HTTP API, a agregaty historyczne trafiaja do lokalnego cache SQLite.</p>
<p className="mt-2">Flask with modular services, without uvicorn and without pydantic-core. InfluxDB reads go through the HTTP API, while historical aggregates are stored in the local SQLite cache.</p>
</div>
<div className="rounded-2xl border border-white/10 bg-slate-950/30 p-4">
<div className="font-medium text-white">Frontend</div>
<p className="mt-2">React + TypeScript + Vite + Tailwind, responsywne karty, live charts i widok mobilny bez osobnej wersji aplikacji.</p>
<p className="mt-2">React + TypeScript + Vite + Tailwind, responsive cards, live charts and mobile view without a separate app version.</p>
</div>
<div className="rounded-2xl border border-white/10 bg-slate-950/30 p-4">
<div className="font-medium text-white">Logika danych</div>
<p className="mt-2">Produkcja dzienna, tygodniowa, miesieczna i roczna jest liczona z surowych danych Influxa na podstawie licznika energii calkowitej, a gdy go brak z mocy AC. Pelne dni sa cache'owane lokalnie.</p>
<div className="font-medium text-white">Data logic</div>
<p className="mt-2">Daily, weekly, monthly and yearly production is calculated from raw Influx data using the total energy counter, and when it is missing, from AC power. Full days are cached locally.</p>
</div>
</div>
</Card>
<Card title="Konfiguracja deploymentu" subtitle="Najwazniejsze ustawienia od razu widoczne">
<Card title="Deployment configuration" subtitle="Most important settings visible at a glance">
<dl className="grid gap-4 text-sm">
<div className="rounded-2xl border border-white/10 bg-slate-950/30 p-4">
<dt className="text-slate-500">Site name</dt>
@@ -41,7 +41,7 @@ export function ConfigPanel({ config }: ConfigPanelProps) {
<dd className="mt-2 text-white">{config.app.timezone}</dd>
</div>
<div className="rounded-2xl border border-white/10 bg-slate-950/30 p-4">
<dt className="text-slate-500">Moduly live / analytics / history</dt>
<dt className="text-slate-500">Live / analytics / history modules</dt>
<dd className="mt-2 text-white">
live: {String(config.capabilities.realtime_enabled)} / analytics: {String(config.capabilities.analytics_enabled)} / history: {String(config.capabilities.historical_import_enabled)}
</dd>
@@ -51,7 +51,7 @@ export function ConfigPanel({ config }: ConfigPanelProps) {
{config.capabilities.historical_import_enabled ? <HistoricalImportPanel config={config} /> : null}
<Card title="Mapowanie encji" subtitle="Tabela pomocnicza do dalszego doprecyzowania integracji" className="xl:col-span-2">
<Card title="Entity mapping" subtitle="Helper table for further integration tuning" className="xl:col-span-2">
<div className="overflow-auto">
<table className="min-w-full text-left text-sm text-slate-300">
<thead>

View File

@@ -7,27 +7,77 @@ import { formatDate, formatDateTime, formatDurationShort, formatPercent, formatV
interface HistoricalImportPanelProps { config: DashboardConfig; }
function eventTone(level?: string): "ok" | "warn" | "critical" | "neutral" { if (level === "success") return "ok"; if (level === "warn") return "warn"; if (level === "error") return "critical"; return "neutral"; }
function chunkTone(state?: string): "ok" | "warn" | "critical" | "neutral" { if (state === "completed") return "ok"; if (state === "running") return "warn"; if (state === "failed") return "critical"; return "neutral"; }
function eventTone(level?: string): "ok" | "warn" | "critical" | "neutral" {
if (level === "success") return "ok";
if (level === "warn") return "warn";
if (level === "error") return "critical";
return "neutral";
}
function chunkTone(state?: string): "ok" | "warn" | "critical" | "neutral" {
if (state === "completed") return "ok";
if (state === "running") return "warn";
if (state === "failed") return "critical";
return "neutral";
}
function StatCard({ label, value, helper }: { label: string; value: string; helper: string }) {
return <div className="rounded-2xl border border-white/10 bg-slate-950/35 p-4"><div className="text-xs uppercase tracking-[0.2em] text-slate-500">{label}</div><div className="mt-3 text-2xl font-semibold text-white">{value}</div><div className="mt-2 text-xs text-slate-500">{helper}</div></div>;
return (
<div className="rounded-2xl border border-white/10 bg-slate-950/35 p-4">
<div className="text-xs uppercase tracking-[0.2em] text-slate-500">{label}</div>
<div className="mt-3 text-2xl font-semibold text-white">{value}</div>
<div className="mt-2 text-xs text-slate-500">{helper}</div>
</div>
);
}
function ChunkRow({ chunk, activeChunkIndex }: { chunk: HistoricalChunkProgress; activeChunkIndex: number }) {
const isActive = chunk.chunk_index === activeChunkIndex || chunk.state === "running";
return (
<div className="rounded-2xl border border-white/10 bg-slate-950/35 p-4">
<div className="flex flex-wrap items-center justify-between gap-3"><div><div className="text-sm font-medium text-white">Chunk {chunk.chunk_index}/{chunk.total_chunks}</div><div className="mt-1 text-xs text-slate-500">{formatDate(chunk.start_date)} - {formatDate(chunk.end_date)}</div></div><div className="flex items-center gap-2">{isActive ? <Badge tone="warn">aktywny</Badge> : null}<Badge tone={chunkTone(chunk.state)}>{chunk.state}</Badge></div></div>
<div className="mt-4 grid gap-3 text-sm sm:grid-cols-4"><div><div className="text-slate-500">Przetworzone</div><div className="mt-1 text-white">{chunk.processed_days}</div></div><div><div className="text-slate-500">Import</div><div className="mt-1 text-white">{chunk.imported_days}</div></div><div><div className="text-slate-500">Pominiete</div><div className="mt-1 text-white">{chunk.skipped_days}</div></div><div><div className="text-slate-500">Energia</div><div className="mt-1 text-white">{formatValue(chunk.energy_kwh, "kWh", 2)}</div></div></div>
<div className="mt-3 flex flex-wrap items-center justify-between gap-3 text-xs text-slate-500"><span>{chunk.note}</span><span>{chunk.duration_seconds ? `czas ${formatDurationShort(chunk.duration_seconds)}` : "w toku"}</span></div>
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="text-sm font-medium text-white">Chunk {chunk.chunk_index}/{chunk.total_chunks}</div>
<div className="mt-1 text-xs text-slate-500">{formatDate(chunk.start_date)} - {formatDate(chunk.end_date)}</div>
</div>
<div className="flex items-center gap-2">
{isActive ? <Badge tone="warn">active</Badge> : null}
<Badge tone={chunkTone(chunk.state)}>{chunk.state}</Badge>
</div>
</div>
<div className="mt-4 grid gap-3 text-sm sm:grid-cols-4">
<div><div className="text-slate-500">Processed</div><div className="mt-1 text-white">{chunk.processed_days}</div></div>
<div><div className="text-slate-500">Imported</div><div className="mt-1 text-white">{chunk.imported_days}</div></div>
<div><div className="text-slate-500">Skipped</div><div className="mt-1 text-white">{chunk.skipped_days}</div></div>
<div><div className="text-slate-500">Energy</div><div className="mt-1 text-white">{formatValue(chunk.energy_kwh, "kWh", 2)}</div></div>
</div>
<div className="mt-3 flex flex-wrap items-center justify-between gap-3 text-xs text-slate-500">
<span>{chunk.note}</span>
<span>{chunk.duration_seconds ? `time ${formatDurationShort(chunk.duration_seconds)}` : "in progress"}</span>
</div>
</div>
);
}
function EventRow({ event }: { event: HistoricalActivityEvent }) {
return (
<div className="relative pl-5"><div className="absolute left-0 top-2 h-2.5 w-2.5 rounded-full bg-white/30" /><div className="rounded-2xl border border-white/10 bg-slate-950/35 p-4"><div className="flex flex-wrap items-center justify-between gap-3"><div className="flex items-center gap-2"><Badge tone={eventTone(event.level)}>{event.level}</Badge><div className="text-sm font-medium text-white">{event.title}</div></div><div className="text-xs text-slate-500">{formatDateTime(event.timestamp)}</div></div><div className="mt-2 text-sm text-slate-300">{event.message}</div><div className="mt-3 flex flex-wrap gap-3 text-xs text-slate-500">{event.day ? <span>Dzien: {formatDate(event.day)}</span> : null}{event.chunk_index ? <span>Chunk: #{event.chunk_index}</span> : null}</div></div></div>
<div className="relative pl-5">
<div className="absolute left-0 top-2 h-2.5 w-2.5 rounded-full bg-white/30" />
<div className="rounded-2xl border border-white/10 bg-slate-950/35 p-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Badge tone={eventTone(event.level)}>{event.level}</Badge>
<div className="text-sm font-medium text-white">{event.title}</div>
</div>
<div className="text-xs text-slate-500">{formatDateTime(event.timestamp)}</div>
</div>
<div className="mt-2 text-sm text-slate-300">{event.message}</div>
<div className="mt-3 flex flex-wrap gap-3 text-xs text-slate-500">
{event.day ? <span>Day: {formatDate(event.day)}</span> : null}
{event.chunk_index ? <span>Chunk: #{event.chunk_index}</span> : null}
</div>
</div>
</div>
);
}
@@ -39,9 +89,14 @@ export function HistoricalImportPanel({ config }: HistoricalImportPanelProps) {
const [chunkDays, setChunkDays] = useState(String(config.capabilities.history.default_chunk_days || 7));
const [force, setForce] = useState(false);
useEffect(() => { setChunkDays(String(config.capabilities.history.default_chunk_days || 7)); }, [config.capabilities.history.default_chunk_days]);
useEffect(() => {
setChunkDays(String(config.capabilities.history.default_chunk_days || 7));
}, [config.capabilities.history.default_chunk_days]);
const progress = useMemo(() => { if (!payload || payload.total_days <= 0) return 0; return Math.min(100, Math.round((payload.processed_days / payload.total_days) * 100)); }, [payload]);
const progress = useMemo(() => {
if (!payload || payload.total_days <= 0) return 0;
return Math.min(100, Math.round((payload.processed_days / payload.total_days) * 100));
}, [payload]);
const visibleChunks = useMemo(() => [...(payload?.recent_chunks ?? [])].sort((l, r) => r.chunk_index - l.chunk_index), [payload?.recent_chunks]);
const visibleEvents = useMemo(() => [...(payload?.recent_events ?? [])].sort((l, r) => new Date(r.timestamp).getTime() - new Date(l.timestamp).getTime()), [payload?.recent_events]);
@@ -50,25 +105,76 @@ export function HistoricalImportPanel({ config }: HistoricalImportPanelProps) {
const availableRangeReady = Boolean(payload?.available_start_date && payload?.available_end_date);
return (
<Card title="Import archiwalny z InfluxDB" subtitle="Mechanizm backfillu dzien po dniu z lokalnym cache SQLite, kontrola chunkow, ETA i lista ostatnich operacji" className="xl:col-span-2">
<div className="grid gap-6 2xl:grid-cols-[1.15fr_0.85fr]"><div className="space-y-6">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<StatCard label="Postep" value={`${progress}%`} helper={`${payload?.processed_days ?? 0} / ${payload?.total_days ?? 0} dni`} />
<StatCard label="Przepustowosc" value={formatValue(payload?.avg_days_per_minute ?? null, "dni/min", 1)} helper="Srednia szybkosc importu" />
<StatCard label="ETA" value={formatDurationShort(payload?.estimated_remaining_seconds)} helper="Szacowany czas do konca" />
<StatCard label="Pokrycie" value={formatPercent(payload?.coverage?.coverage_pct ?? null)} helper={`${payload?.coverage?.missing_days ?? 0} brakujacych dni`} />
<Card title="Historical import from InfluxDB" subtitle="Day-by-day backfill with local SQLite cache, chunk control, ETA and recent activity list" className="xl:col-span-2">
<div className="grid gap-6 2xl:grid-cols-[1.15fr_0.85fr]">
<div className="space-y-6">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<StatCard label="Progress" value={`${progress}%`} helper={`${payload?.processed_days ?? 0} / ${payload?.total_days ?? 0} days`} />
<StatCard label="Throughput" value={formatValue(payload?.avg_days_per_minute ?? null, "days/min", 1)} helper="Average import speed" />
<StatCard label="ETA" value={formatDurationShort(payload?.estimated_remaining_seconds)} helper="Estimated time to completion" />
<StatCard label="Coverage" value={formatPercent(payload?.coverage?.coverage_pct ?? null)} helper={`${payload?.coverage?.missing_days ?? 0} missing days`} />
</div>
<div className="grid gap-4 lg:grid-cols-[0.9fr_1.1fr]">
<div className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<label className="block rounded-2xl border border-white/10 bg-slate-950/30 p-4 text-sm text-slate-300">
<span className="mb-2 block text-slate-500">Start date</span>
<input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950 px-3 py-2 text-white outline-none" />
</label>
<label className="block rounded-2xl border border-white/10 bg-slate-950/30 p-4 text-sm text-slate-300">
<span className="mb-2 block text-slate-500">End date</span>
<input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950 px-3 py-2 text-white outline-none" />
</label>
</div>
<div className="grid gap-4 md:grid-cols-[220px_1fr]">
<label className="block rounded-2xl border border-white/10 bg-slate-950/30 p-4 text-sm text-slate-300">
<span className="mb-2 block text-slate-500">Chunk (days)</span>
<input type="number" min={1} max={31} value={chunkDays} onChange={(e) => setChunkDays(e.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950 px-3 py-2 text-white outline-none" />
</label>
<div className="rounded-2xl border border-white/10 bg-slate-950/30 p-4 text-sm text-slate-300">
<label className="flex items-center gap-3">
<input type="checkbox" checked={force} onChange={(e) => setForce(e.target.checked)} />
<span>Overwrite days already present in the historical cache</span>
</label>
<p className="mt-3 text-slate-500">When date fields are empty, the backend automatically detects the import range based on the first InfluxDB sample and the last day already stored in SQLite.</p>
</div>
</div>
<div className="flex flex-wrap gap-3">
<button type="button" disabled={busy} onClick={() => start.mutate({ start_date: startDate || undefined, end_date: endDate || undefined, chunk_days: Number(chunkDays || 7), force })} className="rounded-full bg-white px-5 py-2.5 text-sm font-medium text-slate-950 transition hover:bg-slate-200 disabled:cursor-not-allowed disabled:opacity-50">Start import</button>
<button type="button" disabled={busy} onClick={() => syncNow.mutate()} className="rounded-full bg-emerald-500/20 px-5 py-2.5 text-sm font-medium text-emerald-200 transition hover:bg-emerald-500/30 disabled:cursor-not-allowed disabled:opacity-50">Sync missing days</button>
<button type="button" disabled={!payload?.running || busy} onClick={() => cancel.mutate()} className="rounded-full bg-rose-500/20 px-5 py-2.5 text-sm font-medium text-rose-200 transition hover:bg-rose-500/30 disabled:cursor-not-allowed disabled:opacity-50">Cancel</button>
{availableRangeReady ? <button type="button" disabled={busy} onClick={() => { setStartDate(payload?.available_start_date ?? ""); setEndDate(payload?.available_end_date ?? ""); }} className="rounded-full bg-sky-500/15 px-5 py-2.5 text-sm font-medium text-sky-200 transition hover:bg-sky-500/25 disabled:cursor-not-allowed disabled:opacity-50">Use full history</button> : null}
</div>
{mutationError ? <div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 p-4 text-sm text-rose-200">{mutationError}</div> : null}
{status.error ? <div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 p-4 text-sm text-rose-200">{status.error.message}</div> : null}
</div>
<div className="rounded-3xl border border-white/10 bg-slate-950/35 p-5">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<div className="text-sm font-medium text-white">Operational task status</div>
<div className="mt-1 text-xs text-slate-500">{payload?.message || "No active task"}</div>
</div>
<div className="flex items-center gap-2">
{payload?.job_id ? <Badge tone="neutral">job {payload.job_id}</Badge> : null}
<Badge tone={payload?.running ? "warn" : payload?.state === "failed" ? "critical" : "neutral"}>{payload?.running ? "running" : payload?.state || "idle"}</Badge>
</div>
</div>
<div className="mt-5 h-3 overflow-hidden rounded-full bg-white/8"><div className="h-full rounded-full bg-gradient-to-r from-emerald-300 via-sky-400 to-cyan-400 transition-all" style={{ width: `${progress}%` }} /></div>
<div className="mt-2 flex items-center justify-between text-xs text-slate-500"><span>Chunk {payload?.active_chunk_index ?? 0} / {payload?.total_chunks ?? 0}</span><span>{progress}%</span></div>
<div className="mt-5 grid gap-4 text-sm text-slate-300 md:grid-cols-2">
<div className="rounded-2xl border border-white/10 bg-slate-900/70 p-4"><div className="text-slate-500">Available range in InfluxDB</div><div className="mt-2 text-white">{formatDate(payload?.available_start_date)} - {formatDate(payload?.available_end_date)}</div><div className="mt-2 text-xs text-slate-500">{payload?.coverage?.available_days ?? 0} detected archive days</div></div>
<div className="rounded-2xl border border-white/10 bg-slate-900/70 p-4"><div className="text-slate-500">Range stored locally</div><div className="mt-2 text-white">{formatDate(payload?.coverage?.first_day)} - {formatDate(payload?.coverage?.last_day)}</div><div className="mt-2 text-xs text-slate-500">{payload?.coverage?.imported_days ?? 0} days in cache, {formatValue(payload?.coverage?.total_energy_kwh ?? 0, "kWh", 1)}</div></div>
<div className="rounded-2xl border border-white/10 bg-slate-900/70 p-4"><div className="text-slate-500">Current chunk</div><div className="mt-2 text-white">{formatDate(payload?.current_chunk_start)} - {formatDate(payload?.current_chunk_end)}</div><div className="mt-2 text-xs text-slate-500">Last day: {formatDate(payload?.current_date)}</div></div>
<div className="rounded-2xl border border-white/10 bg-slate-900/70 p-4"><div className="text-slate-500">Timings and delays</div><div className="mt-2 text-white">elapsed {formatDurationShort(payload?.elapsed_seconds)}</div><div className="mt-2 text-xs text-slate-500">start {formatDateTime(payload?.started_at)} / finish {formatDateTime(payload?.finished_at)}</div>{payload?.last_error ? <div className="mt-3 text-xs text-rose-200">Error: {payload.last_error}</div> : null}</div>
</div>
</div>
</div>
<div className="grid gap-6 xl:grid-cols-[1.05fr_0.95fr]">
<Card title="Chunk list" subtitle="Latest ranges with imported days count and energy per chunk"><div className="space-y-3">{visibleChunks.length ? visibleChunks.map((chunk) => <ChunkRow key={chunk.chunk_index} chunk={chunk} activeChunkIndex={payload?.active_chunk_index ?? 0} />) : <div className="rounded-2xl border border-dashed border-white/10 bg-slate-950/20 p-6 text-sm text-slate-400">The chunk list will appear after the first backfill starts.</div>}</div></Card>
<Card title="Recent operations" subtitle="Recent days, chunks and warnings without opening backend logs"><div className="space-y-3 border-l border-white/10 pl-4">{visibleEvents.length ? visibleEvents.map((event, index) => <EventRow key={`${event.timestamp}-${index}`} event={event} />) : <div className="rounded-2xl border border-dashed border-white/10 bg-slate-950/20 p-6 text-sm text-slate-400">Operation history will appear after the task starts.</div>}</div></Card>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-[0.9fr_1.1fr]"><div className="space-y-4">
<div className="grid gap-4 md:grid-cols-2"><label className="block rounded-2xl border border-white/10 bg-slate-950/30 p-4 text-sm text-slate-300"><span className="mb-2 block text-slate-500">Data od</span><input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950 px-3 py-2 text-white outline-none" /></label><label className="block rounded-2xl border border-white/10 bg-slate-950/30 p-4 text-sm text-slate-300"><span className="mb-2 block text-slate-500">Data do</span><input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950 px-3 py-2 text-white outline-none" /></label></div>
<div className="grid gap-4 md:grid-cols-[220px_1fr]"><label className="block rounded-2xl border border-white/10 bg-slate-950/30 p-4 text-sm text-slate-300"><span className="mb-2 block text-slate-500">Chunk (dni)</span><input type="number" min={1} max={31} value={chunkDays} onChange={(e) => setChunkDays(e.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950 px-3 py-2 text-white outline-none" /></label><div className="rounded-2xl border border-white/10 bg-slate-950/30 p-4 text-sm text-slate-300"><label className="flex items-center gap-3"><input type="checkbox" checked={force} onChange={(e) => setForce(e.target.checked)} /><span>Nadpisz dni juz obecne w cache historycznym</span></label><p className="mt-3 text-slate-500">Gdy pola dat sa puste, backend sam wykryje zakres do importu na podstawie pierwszej probki w InfluxDB i ostatniego dnia juz zapisanego w SQLite.</p></div></div>
<div className="flex flex-wrap gap-3"><button type="button" disabled={busy} onClick={() => start.mutate({ start_date: startDate || undefined, end_date: endDate || undefined, chunk_days: Number(chunkDays || 7), force })} className="rounded-full bg-white px-5 py-2.5 text-sm font-medium text-slate-950 transition hover:bg-slate-200 disabled:cursor-not-allowed disabled:opacity-50">Start importu</button><button type="button" disabled={busy} onClick={() => syncNow.mutate()} className="rounded-full bg-emerald-500/20 px-5 py-2.5 text-sm font-medium text-emerald-200 transition hover:bg-emerald-500/30 disabled:cursor-not-allowed disabled:opacity-50">Synchronizuj brakujace dni</button><button type="button" disabled={!payload?.running || busy} onClick={() => cancel.mutate()} className="rounded-full bg-rose-500/20 px-5 py-2.5 text-sm font-medium text-rose-200 transition hover:bg-rose-500/30 disabled:cursor-not-allowed disabled:opacity-50">Anuluj</button>{availableRangeReady ? <button type="button" disabled={busy} onClick={() => { setStartDate(payload?.available_start_date ?? ""); setEndDate(payload?.available_end_date ?? ""); }} className="rounded-full bg-sky-500/15 px-5 py-2.5 text-sm font-medium text-sky-200 transition hover:bg-sky-500/25 disabled:cursor-not-allowed disabled:opacity-50">Ustaw pelna historie</button> : null}</div>
{mutationError ? <div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 p-4 text-sm text-rose-200">{mutationError}</div> : null}
{status.error ? <div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 p-4 text-sm text-rose-200">{status.error.message}</div> : null}
</div>
<div className="rounded-3xl border border-white/10 bg-slate-950/35 p-5"><div className="flex flex-wrap items-start justify-between gap-3"><div><div className="text-sm font-medium text-white">Operacyjny status zadania</div><div className="mt-1 text-xs text-slate-500">{payload?.message || "Brak aktywnego zadania"}</div></div><div className="flex items-center gap-2">{payload?.job_id ? <Badge tone="neutral">job {payload.job_id}</Badge> : null}<Badge tone={payload?.running ? "warn" : payload?.state === "failed" ? "critical" : "neutral"}>{payload?.running ? "w trakcie" : payload?.state || "idle"}</Badge></div></div><div className="mt-5 h-3 overflow-hidden rounded-full bg-white/8"><div className="h-full rounded-full bg-gradient-to-r from-emerald-300 via-sky-400 to-cyan-400 transition-all" style={{ width: `${progress}%` }} /></div><div className="mt-2 flex items-center justify-between text-xs text-slate-500"><span>Chunk {payload?.active_chunk_index ?? 0} / {payload?.total_chunks ?? 0}</span><span>{progress}%</span></div>
<div className="mt-5 grid gap-4 text-sm text-slate-300 md:grid-cols-2"><div className="rounded-2xl border border-white/10 bg-slate-900/70 p-4"><div className="text-slate-500">Dostepny zakres w InfluxDB</div><div className="mt-2 text-white">{formatDate(payload?.available_start_date)} - {formatDate(payload?.available_end_date)}</div><div className="mt-2 text-xs text-slate-500">{payload?.coverage?.available_days ?? 0} dni wykrytego archiwum</div></div><div className="rounded-2xl border border-white/10 bg-slate-900/70 p-4"><div className="text-slate-500">Zakres zapisany lokalnie</div><div className="mt-2 text-white">{formatDate(payload?.coverage?.first_day)} - {formatDate(payload?.coverage?.last_day)}</div><div className="mt-2 text-xs text-slate-500">{payload?.coverage?.imported_days ?? 0} dni w cache, {formatValue(payload?.coverage?.total_energy_kwh ?? 0, "kWh", 1)}</div></div><div className="rounded-2xl border border-white/10 bg-slate-900/70 p-4"><div className="text-slate-500">Aktualny chunk</div><div className="mt-2 text-white">{formatDate(payload?.current_chunk_start)} - {formatDate(payload?.current_chunk_end)}</div><div className="mt-2 text-xs text-slate-500">Ostatni dzien: {formatDate(payload?.current_date)}</div></div><div className="rounded-2xl border border-white/10 bg-slate-900/70 p-4"><div className="text-slate-500">Czasy i opoznienia</div><div className="mt-2 text-white">elapsed {formatDurationShort(payload?.elapsed_seconds)}</div><div className="mt-2 text-xs text-slate-500">start {formatDateTime(payload?.started_at)} / koniec {formatDateTime(payload?.finished_at)}</div>{payload?.last_error ? <div className="mt-3 text-xs text-rose-200">Blad: {payload.last_error}</div> : null}</div></div></div></div>
<div className="grid gap-6 xl:grid-cols-[1.05fr_0.95fr]"><Card title="Lista chunkow" subtitle="Najswiezsze zakresy z liczba zaimportowanych dni i energia na chunk"><div className="space-y-3">{visibleChunks.length ? visibleChunks.map((chunk) => <ChunkRow key={chunk.chunk_index} chunk={chunk} activeChunkIndex={payload?.active_chunk_index ?? 0} />) : <div className="rounded-2xl border border-dashed border-white/10 bg-slate-950/20 p-6 text-sm text-slate-400">Lista chunkow pojawi sie po uruchomieniu pierwszego backfillu.</div>}</div></Card><Card title="Ostatnie operacje" subtitle="Operator widzi ostatnie dni, chunki i ewentualne ostrzezenia bez wchodzenia do logow backendu"><div className="space-y-3 border-l border-white/10 pl-4">{visibleEvents.length ? visibleEvents.map((event, index) => <EventRow key={`${event.timestamp}-${index}`} event={event} />) : <div className="rounded-2xl border border-dashed border-white/10 bg-slate-950/20 p-6 text-sm text-slate-400">Historia operacji pojawi sie po starcie zadania.</div>}</div></Card></div>
</div></div>
</div>
</Card>
);
}