first commit

This commit is contained in:
Mateusz Gruszczyński
2026-03-23 15:56:18 +01:00
commit c5cc2efbac
106 changed files with 10254 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
import clsx from "clsx";
import { Card } from "../common/Card";
import { formatValue } from "../../lib/format";
import type { HeroCard } from "../../types";
interface HeroKpiGridProps {
cards: HeroCard[];
}
const accents: Record<string, string> = {
emerald: "from-emerald-400/15 to-emerald-500/5 ring-emerald-300/20",
amber: "from-amber-400/15 to-amber-500/5 ring-amber-300/20",
rose: "from-rose-400/15 to-rose-500/5 ring-rose-300/20",
slate: "from-white/10 to-white/5 ring-white/10",
};
export function HeroKpiGrid({ cards }: HeroKpiGridProps) {
return (
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
{cards.map((card) => (
<Card
key={card.metric_id}
className={clsx("overflow-hidden bg-gradient-to-br", accents[card.accent] ?? accents.slate)}
>
<div className="text-xs uppercase tracking-[0.22em] text-slate-400">{card.label}</div>
<div className="mt-3 text-3xl font-semibold text-white">{formatValue(card.value, card.unit, 2)}</div>
<div className="mt-3 text-sm text-slate-400">{card.subtitle}</div>
</Card>
))}
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { Card } from "../common/Card";
import { formatValue } from "../../lib/format";
import type { MetricValue } from "../../types";
interface KpiStripProps {
items: Record<string, MetricValue>;
}
const order = [
"energy_today",
"energy_yesterday",
"today_vs_yesterday",
"dc_power_total",
"energy_total",
];
export function KpiStrip({ items }: KpiStripProps) {
return (
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
{order
.filter((metricId) => items[metricId])
.map((metricId) => {
const metric = items[metricId];
return (
<Card key={metric.metric_id} className="bg-slate-950/35">
<div className="text-xs uppercase tracking-[0.18em] text-slate-500">{metric.label}</div>
<div className="mt-3 text-2xl font-semibold text-white">
{formatValue(metric.value, metric.unit, metric.precision)}
</div>
</Card>
);
})}
</div>
);
}

View File

@@ -0,0 +1,64 @@
import type { EChartsOption } from "echarts";
import { Card } from "../common/Card";
import { EChart } from "../common/EChart";
import type { HistoryPayload } from "../../types";
interface LiveHistoryChartProps {
history?: HistoryPayload;
title?: string;
}
export function LiveHistoryChart({ history, title = "Dane chwilowe" }: LiveHistoryChartProps) {
const option: EChartsOption = {
tooltip: {
trigger: "axis",
backgroundColor: "rgba(2, 6, 23, 0.95)",
borderColor: "rgba(255,255,255,0.08)",
textStyle: { color: "#e2e8f0" },
},
legend: {
top: 0,
textStyle: { color: "#cbd5e1" },
},
grid: {
left: 18,
right: 16,
top: 46,
bottom: 24,
containLabel: true,
},
xAxis: {
type: "category",
boundaryGap: false,
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" })
) ?? [],
},
yAxis: {
type: "value",
axisLabel: { color: "#94a3b8" },
splitLine: { lineStyle: { color: "rgba(255,255,255,0.06)" } },
},
series:
history?.series.map((item) => ({
name: `${item.label} (${item.unit})`,
type: "line",
smooth: true,
showSymbol: false,
lineStyle: { width: 3 },
areaStyle: { opacity: 0.08 },
data: item.points.map((point) => point.value),
})) ?? [],
};
return (
<Card
title={title}
subtitle="Moc AC, moce stringow DC i opcjonalnie temperatura falownika w jednym widoku live"
>
<EChart option={option} className="h-[340px] w-full" />
</Card>
);
}

View File

@@ -0,0 +1,30 @@
import { Badge } from "../common/Badge";
import { Card } from "../common/Card";
import { formatValue } from "../../lib/format";
import type { MetricValue } from "../../types";
interface LiveStatusBoardProps {
status: MetricValue[];
}
export function LiveStatusBoard({ status }: LiveStatusBoardProps) {
if (!status.length) {
return null;
}
return (
<Card title="Stan systemu" subtitle="Temperatura falownika i kontrola swiezosci odczytu">
<div className="grid gap-3 sm:grid-cols-2">
{status.map((metric) => (
<div key={metric.metric_id} className="rounded-2xl border border-white/10 bg-slate-950/30 p-4">
<div className="mb-3 flex items-center justify-between gap-3">
<div className="text-sm font-medium text-white">{metric.label}</div>
<Badge tone={metric.status}>{metric.status}</Badge>
</div>
<div className="text-sm text-slate-300">{formatValue(metric.value, metric.unit, metric.precision)}</div>
</div>
))}
</div>
</Card>
);
}

View File

@@ -0,0 +1,26 @@
import { Card } from "../common/Card";
import { ValuePair } from "../common/ValuePair";
import type { SnapshotGroupRow } from "../../types";
interface PhaseGridProps {
rows: SnapshotGroupRow[];
}
export function PhaseGrid({ rows }: PhaseGridProps) {
return (
<Card title="Fazy AC" subtitle="Napiece, prady 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">
<div className="mb-4 text-sm font-semibold text-white">{row.label}</div>
<div className="grid gap-3">
<ValuePair metric={row.values.voltage} />
<ValuePair metric={row.values.current} />
<ValuePair metric={row.values.apparent_power} />
</div>
</div>
))}
</div>
</Card>
);
}

View File

@@ -0,0 +1,34 @@
import { Card } from "../common/Card";
import { ValuePair } from "../common/ValuePair";
import type { SnapshotGroupRow } from "../../types";
interface StringGridProps {
rows: SnapshotGroupRow[];
}
const slotOrder = ["power", "voltage"] as const;
export function StringGrid({ rows }: StringGridProps) {
return (
<Card title="Stringi DC" subtitle="Widok automatycznie skaluje sie do liczby stringow i dostepnych 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]);
return (
<div key={row.id} className="rounded-3xl border border-white/10 bg-slate-950/40 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="text-sm font-semibold text-white">{row.label}</div>
<div className="text-xs uppercase tracking-[0.18em] text-slate-500">{row.id}</div>
</div>
<div className={`grid gap-3 ${visibleSlots.length > 1 ? "sm:grid-cols-2" : "sm:grid-cols-1"}`}>
{visibleSlots.map((slot) => (
<ValuePair key={slot} metric={row.values[slot]} />
))}
</div>
</div>
);
})}
</div>
</Card>
);
}