Initiale Version: Stromverbrauch-Web-App (strom-next)

Next.js-App zur Visualisierung des Stromverbrauchs aus InfluxDB
(Bucket strom, measurement vzlogger, Feld arbeit = kumulativer
Zaehlerstand in Wh).

- 4 Balken-Diagramme: 24h (stuendlich), Woche (stuendlich),
  31 Tage (taeglich), Jahr (pro Monat, immer 12 Monate)
- Verbrauch via difference des Zaehlerstands, zeitzonen-korrekt
- 24h liest Rohdaten (aktuell), 7d/31d/365d den stuendlichen
  Downsampling-Rollup (arbeit_hourly) -> sub-sekunde
- Gesamtverbrauch je Zeitraum unter dem Chart
- Design/Layout angelehnt an Werte-Log "Verlauf"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 14:05:27 +02:00
commit 3649bf4e22
19 changed files with 7659 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from 'next/server';
import { resolveRange } from '@/lib/ranges';
import { fetchVerbrauch } from '@/lib/influx';
import { RangeKey } from '@/types/strom';
const VALID_RANGES: RangeKey[] = ['24h', '7d', '31d', '365d'];
// GET /api/verbrauch?range=24h&date=YYYY-MM-DD
// ?range=7d&date=YYYY-MM-DD
// ?range=31d&month=YYYY-MM
// ?range=365d&year=YYYY
export async function GET(request: NextRequest) {
const sp = request.nextUrl.searchParams;
const range = sp.get('range') as RangeKey | null;
if (!range || !VALID_RANGES.includes(range)) {
return NextResponse.json(
{ success: false, error: `Ungueltiger range-Parameter. Erlaubt: ${VALID_RANGES.join(', ')}` },
{ status: 400 }
);
}
try {
const resolved = resolveRange(range, {
date: sp.get('date'),
month: sp.get('month'),
year: sp.get('year'),
});
const data = await fetchVerbrauch(resolved);
return NextResponse.json(
{ success: true, data },
{
headers: {
'Cache-Control': 'no-store, no-cache, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
},
}
);
} catch (error) {
console.error('InfluxDB error:', error);
return NextResponse.json(
{ success: false, error: 'Fehler beim Laden der Verbrauchsdaten.' },
{ status: 500 }
);
}
}
+22
View File
@@ -0,0 +1,22 @@
/* stylelint-disable at-rule-no-unknown */
@import "tailwindcss";
@source "../components/**/*.tsx";
@source "../app/**/*.tsx";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
+34
View File
@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Stromverbrauch",
description: "Visualisierung des Stromverbrauchs",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="de">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
+16
View File
@@ -0,0 +1,16 @@
'use client';
import dynamic from 'next/dynamic';
const VerbrauchCharts = dynamic(() => import('@/components/VerbrauchCharts'), {
ssr: false,
loading: () => (
<div className="min-h-screen bg-white flex items-center justify-center">
<span className="text-gray-500">Lade Grafiken</span>
</div>
),
});
export default function Home() {
return <VerbrauchCharts />;
}