Weather Station / ECOWITT / DNT
0

Configure Feed

Select the types of activity you want to include in your feed.

docs: Add comprehensive JSDoc documentation to the entire codebase

This change adds JSDoc comments to all functions, classes, types, and components across the entire repository. It also updates the README.md file to include a section on the code structure and documentation, making the project more accessible to new developers.

+1190 -47
+13
README.md
··· 91 91 - Common date format `YYYY/M/D H:MM` (dashboard also supports ISO-like variants) 92 92 - German headers (e.g., `Zeit`, `Luftfeuchtigkeit`, `Taupunkt`, `Wärmeindex`) 93 93 94 + ### Code Structure and Documentation 95 + 96 + The project is structured to separate concerns, making it easier to navigate and maintain. All functions, classes, and components are fully documented with JSDoc comments. 97 + 98 + - **`src/app`**: Contains the main application pages, layouts, and API routes. 99 + - **`src/app/api`**: All server-side API logic resides here. Each subdirectory corresponds to a specific endpoint. 100 + - **`src/components`**: Reusable React components used throughout the application. 101 + - **`src/lib`**: Core logic, utilities, and third-party library configurations. 102 + - **`src/lib/db`**: Database-related logic, including data ingestion and querying with DuckDB. 103 + - **`src/contexts`**: React context providers for managing global state. 104 + - **`src/scripts`**: Standalone scripts for tasks like pre-warming the data cache. 105 + - **`src/types`**: TypeScript type definitions, including declarations for external modules. 106 + 94 107 ## Channel name configuration 95 108 96 109 - File: `src/config/channels.json`
+11
src/app/api/config/channels/route.ts
··· 5 5 6 6 export const runtime = "nodejs"; 7 7 8 + /** 9 + * API route to get the channel configuration. 10 + * @returns {Promise<NextResponse>} A JSON response containing the channel configuration. 11 + * @example 12 + * // GET /api/config/channels 13 + * // Returns: 14 + * // { 15 + * // "ch1": { "name": "Garten" }, 16 + * // "ch2": { "name": "Keller" } 17 + * // } 18 + */ 8 19 export async function GET() { 9 20 const abs = path.join(process.cwd(), "src", "config", "channels.json"); 10 21 const text = await fs.readFile(abs, "utf8");
+17
src/app/api/data/allsensors/route.ts
··· 8 8 9 9 export const runtime = "nodejs"; 10 10 11 + /** 12 + * API route to get aggregated 'allsensors' data. 13 + * This function handles GET requests to /api/data/allsensors. 14 + * It can filter data by month or by a time range, and aggregate it by minute, hour, or day. 15 + * It first attempts to use a fast path with DuckDB and Parquet files, and falls back to parsing CSV files if that fails. 16 + * 17 + * @param {Request} req - The incoming request object. 18 + * @returns {Promise<NextResponse>} A JSON response containing the aggregated data, or an error message. 19 + * 20 + * @example 21 + * // Get data for a specific month with hourly resolution 22 + * GET /api/data/allsensors?month=202508&resolution=hour 23 + * 24 + * @example 25 + * // Get data for a specific time range with daily resolution 26 + * GET /api/data/allsensors?start=2025-08-01T00:00:00&end=2025-08-15T23:59:59&resolution=day 27 + */ 11 28 export async function GET(req: Request) { 12 29 try { 13 30 const { searchParams } = new URL(req.url);
+18
src/app/api/data/extent/route.ts
··· 6 6 7 7 export const runtime = "nodejs"; 8 8 9 + /** 10 + * Formats a Date object into an ISO-like string up to the minute (YYYY-MM-DDTHH:mm). 11 + * @param {Date} d - The date to format. 12 + * @returns {string} The formatted date string. 13 + * @private 14 + */ 9 15 function toIsoMinute(d: Date) { 10 16 const yyyy = d.getFullYear(); 11 17 const mm = String(d.getMonth() + 1).padStart(2, "0"); ··· 15 21 return `${yyyy}-${mm}-${dd}T${hh}:${mi}`; 16 22 } 17 23 24 + /** 25 + * API route to get the global time range (min and max timestamps) of all available data. 26 + * It scans the first and last CSV files to find the earliest and latest timestamps. 27 + * @returns {Promise<NextResponse>} A JSON response with `min` and `max` timestamps, or an error. 28 + * @example 29 + * // GET /api/data/extent 30 + * // Returns: 31 + * // { 32 + * // "min": "2024-01-01T00:00", 33 + * // "max": "2025-08-15T14:30" 34 + * // } 35 + */ 18 36 export async function GET() { 19 37 try { 20 38 const mainFiles = await getMainFilesInRange();
+17
src/app/api/data/main/route.ts
··· 8 8 9 9 export const runtime = "nodejs"; 10 10 11 + /** 12 + * API route to get aggregated 'main' sensor data. 13 + * This function handles GET requests to /api/data/main. 14 + * It can filter data by month or by a time range, and aggregate it by minute, hour, or day. 15 + * It first attempts to use a fast path with DuckDB and Parquet files, and falls back to parsing CSV files if that fails. 16 + * 17 + * @param {Request} req - The incoming request object. 18 + * @returns {Promise<NextResponse>} A JSON response containing the aggregated data, or an error message. 19 + * 20 + * @example 21 + * // Get data for a specific month with daily resolution 22 + * GET /api/data/main?month=202508&resolution=day 23 + * 24 + * @example 25 + * // Get data for a specific time range with hourly resolution 26 + * GET /api/data/main?start=2025-08-01T00:00:00&end=2025-08-15T23:59:59&resolution=hour 27 + */ 11 28 export async function GET(req: Request) { 12 29 try { 13 30 const { searchParams } = new URL(req.url);
+12
src/app/api/data/months/route.ts
··· 3 3 4 4 export const runtime = "nodejs"; 5 5 6 + /** 7 + * API route to get a list of available months from the CSV filenames. 8 + * It scans the DNT directory, extracts the YYYYMM prefix from filenames, 9 + * and returns a unique, sorted list of months. 10 + * @returns {Promise<NextResponse>} A JSON response containing an array of month strings. 11 + * @example 12 + * // GET /api/data/months 13 + * // Returns: 14 + * // { 15 + * // "months": ["202508", "202507", "202506"] 16 + * // } 17 + */ 6 18 export async function GET() { 7 19 const files = await listDntFiles(); 8 20 const set = new Set<string>();
+20
src/app/api/device/info/route.ts
··· 4 4 export const dynamic = "force-dynamic"; 5 5 export const runtime = "nodejs"; 6 6 7 + /** 8 + * Builds the URL for the Ecowitt device info API endpoint. 9 + * @returns {string} The full URL for the API request. 10 + * @private 11 + */ 7 12 function buildDeviceInfoUrl() { 8 13 const eco = EcoCon.getInstance().getConfig(); 9 14 const params = new URLSearchParams({ ··· 16 21 return `${baseUrl}?${params.toString()}`; 17 22 } 18 23 24 + /** 25 + * API route to get device information from the Ecowitt API. 26 + * Fetches timezone, latitude, and longitude for the weather station. 27 + * @returns {Promise<NextResponse>} A JSON response containing the device info, or an error. 28 + * @example 29 + * // GET /api/device/info 30 + * // Returns: 31 + * // { 32 + * // "ok": true, 33 + * // "timezone": "Europe/Berlin", 34 + * // "latitude": 52.52, 35 + * // "longitude": 13.40, 36 + * // "raw": { ...full api response... } 37 + * // } 38 + */ 19 39 export async function GET() { 20 40 try { 21 41 const url = buildDeviceInfoUrl();
+13
src/app/api/rt/last/route.ts
··· 4 4 export const dynamic = "force-dynamic"; 5 5 export const runtime = "nodejs"; 6 6 7 + /** 8 + * API route to get the last cached real-time weather data. 9 + * This provides a fast way for the client to get the latest data without hitting the Ecowitt API directly. 10 + * @returns {Promise<NextResponse>} A JSON response containing the last cached data, or an error if no data is available yet. 11 + * @example 12 + * // GET /api/rt/last 13 + * // Returns: 14 + * // { 15 + * // "ok": true, 16 + * // "updatedAt": "2025-08-15T14:30:00.000Z", 17 + * // "data": { ... weather data payload ... } 18 + * // } 19 + */ 7 20 export async function GET() { 8 21 try { 9 22 const last = await getLastRealtime();
+16
src/app/api/rt/route.ts
··· 7 7 8 8 // (archiving logic moved to shared module) 9 9 10 + /** 11 + * API route to proxy real-time data requests to the Ecowitt API. 12 + * This acts as a server-side proxy to hide API credentials from the client. 13 + * It can fetch either all data or a subset based on the `all` query parameter. 14 + * 15 + * @param {Request} req - The incoming request object. 16 + * @returns {Promise<NextResponse>} A JSON response containing the real-time data from the Ecowitt API. 17 + * 18 + * @example 19 + * // Get all real-time data 20 + * GET /api/rt?all=1 21 + * 22 + * @example 23 + * // Get a subset of real-time data 24 + * GET /api/rt 25 + */ 10 26 export async function GET(req: Request) { 11 27 try { 12 28 const url = new URL(req.url);
+14
src/app/api/temp-minmax/route.ts
··· 1 1 import { NextRequest, NextResponse } from 'next/server'; 2 2 import { getTodayTempMinMax, getAllTempMinMax } from '@/lib/temp-minmax'; 3 3 4 + /** 5 + * API route to get the daily minimum and maximum temperature and humidity data. 6 + * By default, it returns the data for the current day. 7 + * It can also return all stored data if the `all=true` query parameter is provided. 8 + * @param {NextRequest} request - The incoming request object. 9 + * @returns {Promise<NextResponse>} A JSON response containing the min/max data, or an error. 10 + * @example 11 + * // Get today's min/max data 12 + * GET /api/temp-minmax 13 + * 14 + * @example 15 + * // Get all stored min/max data 16 + * GET /api/temp-minmax?all=true 17 + */ 4 18 export async function GET(request: NextRequest) { 5 19 try { 6 20 const { searchParams } = new URL(request.url);
+13
src/app/api/temp-minmax/trigger/route.ts
··· 2 2 import { updateTempMinMax, getTodayTempMinMax } from '@/lib/temp-minmax'; 3 3 import { getLastRealtime } from '@/lib/realtimeArchiver'; 4 4 5 + /** 6 + * API route to manually trigger an update of the daily min/max temperature and humidity data. 7 + * It uses the last cached real-time data to perform the update. 8 + * @returns {Promise<NextResponse>} A JSON response indicating success or failure, and the updated data. 9 + * @example 10 + * // POST /api/temp-minmax/trigger 11 + * // Returns: 12 + * // { 13 + * // "ok": true, 14 + * // "data": { ... updated min/max data ... }, 15 + * // "message": "Min/max updated successfully" 16 + * // } 17 + */ 5 18 export async function POST() { 6 19 try { 7 20 // Get current realtime data directly
+17 -1
src/app/api/temp-minmax/update/route.ts
··· 2 2 import { updateTempMinMax, getTodayTempMinMax } from '@/lib/temp-minmax'; 3 3 import { getLastRealtime } from '@/lib/realtimeArchiver'; 4 4 5 + /** 6 + * API route to update and then retrieve the daily min/max temperature and humidity data. 7 + * This is triggered to ensure the min/max values are current based on the latest real-time data. 8 + * @returns {Promise<NextResponse>} A JSON response with the updated min/max data. 9 + * @example 10 + * // POST /api/temp-minmax/update 11 + * // Returns: 12 + * // { 13 + * // "ok": true, 14 + * // "data": { ... updated min/max data ... }, 15 + * // "message": "All temperatures updated successfully" 16 + * // } 17 + */ 5 18 export async function POST() { 6 19 try { 7 20 // Get current realtime data directly ··· 22 35 } 23 36 } 24 37 25 - // Also allow GET for convenience 38 + /** 39 + * Also allow GET for convenience. See POST for details. 40 + * @returns {Promise<NextResponse>} A JSON response with the updated min/max data. 41 + */ 26 42 export async function GET() { 27 43 return POST(); 28 44 }
+12
src/app/layout.tsx
··· 13 13 subsets: ["latin"], 14 14 }); 15 15 16 + /** 17 + * The metadata for the application. 18 + * This includes the title and description for the web page. 19 + */ 16 20 export const metadata: Metadata = { 17 21 title: "Ecowitt Weather Dashboard", 18 22 description: "Dashboard for Ecowitt weather data", 19 23 }; 20 24 25 + /** 26 + * The root layout for the application. 27 + * It sets up the HTML structure, fonts, and the internationalization provider. 28 + * 29 + * @param props - The component props. 30 + * @param props.children - The child components to be rendered within the layout. 31 + * @returns The root layout component. 32 + */ 21 33 export default function RootLayout({ 22 34 children, 23 35 }: Readonly<{
+9
src/app/page.tsx
··· 8 8 import LanguageSwitcher from "@/components/LanguageSwitcher"; 9 9 import { RealtimeProvider } from "@/contexts/RealtimeContext"; 10 10 11 + /** 12 + * The main page component for the weather dashboard. 13 + * It provides a tabbed interface to switch between different views: 14 + * - Realtime: A list of current sensor readings. 15 + * - Graphics: A set of gauges and visual displays for current data. 16 + * - Saved: A dashboard for viewing historical data with charts. 17 + * 18 + * @returns The Home page component. 19 + */ 11 20 export default function Home() { 12 21 const { t } = useTranslation(); 13 22 const [tab, setTab] = useState<"rt" | "gfx" | "stored">("rt");
+269 -12
src/components/Dashboard.tsx
··· 6 6 import LineChart, { type LineSeries } from "@/components/LineChartChartJS"; 7 7 import { CheckIcon } from "@heroicons/react/24/outline"; 8 8 9 + /** 10 + * Represents the response from the months API endpoint. 11 + * @private 12 + */ 9 13 type MonthsResp = { months: string[] }; 10 14 15 + /** 16 + * Represents the response from the data API endpoints. 17 + * @private 18 + */ 11 19 type DataResp = { 12 20 file: string; 13 21 header: string[]; 14 22 rows: Array<Record<string, number | string | null>>; // time as string, numeric values averaged 15 23 }; 16 24 25 + /** 26 + * Represents the channel configuration. 27 + * @private 28 + */ 17 29 type ChannelsConfig = Record<string, { name: string }>; // { ch1: { name: "Living" }, ... } 18 30 31 + /** 32 + * Renders the charts for a single channel card. 33 + * @private 34 + */ 19 35 function renderChannelCardCharts( 20 36 data: DataResp, 21 37 channelsCfg: ChannelsConfig, ··· 33 49 const fmt = makeTimeTickFormatter(xBase, spanMin, locale); 34 50 const hoverFmt = makeHoverTimeFormatter(xBase, locale); 35 51 36 - // Temperaturmetriken gruppieren 52 + // Group temperature metrics 37 53 const tempMetrics: ChannelMetric[] = ["Temperatur", "Taupunkt", "Gefühlte Temperatur"]; 38 54 const chNum = (chKey.match(/\d+/)?.[0]) || "1"; 39 55 const out: React.ReactNode[] = []; 40 56 41 - // Temperaturdiagramm erstellen 57 + // Create temperature chart 42 58 const tempSeries: LineSeries[] = []; 43 59 for (let i = 0; i < tempMetrics.length; i++) { 44 60 const metric = tempMetrics[i]; ··· 342 358 return <>{out}</>; 343 359 } 344 360 361 + /** 362 + * Renders the global time range selection controls. 363 + * This includes datetime-local inputs and range sliders to define a start and end time. 364 + * @param props - The component props. 365 + * @param props.min - The absolute minimum date for the range. 366 + * @param props.max - The absolute maximum date for the range. 367 + * @param props.pctStart - The start of the selected range as a percentage (0-1000) of the total range. 368 + * @param props.pctEnd - The end of the selected range as a percentage (0-1000) of the total range. 369 + * @param props.setPctStart - Callback to set the start percentage. 370 + * @param props.setPctEnd - Callback to set the end percentage. 371 + * @param props.setDateRangeChanged - Optional callback to signal that the date range has been modified by the user. 372 + * @param props.setConfirmButtonActive - Optional callback to activate the confirmation button. 373 + * @returns A React component for global range controls. 374 + * @private 375 + */ 345 376 function GlobalRangeControls(props: { 346 377 min: Date | null; 347 378 max: Date | null; ··· 433 464 ); 434 465 } 435 466 467 + /** 468 + * Pads a number with a leading zero if it's less than 10. 469 + * @param n - The number to pad. 470 + * @returns The padded string. 471 + * @private 472 + */ 436 473 function pad2(n: number) { return n < 10 ? `0${n}` : String(n); } 437 474 438 - // Hilfsfunktionen für Statistikberechnung 475 + /** 476 + * Calculates temperature statistics from the provided data. 477 + * This includes counting days above 30°C and below 0°C, and finding the min/max temperatures. 478 + * @param rows - The data rows. 479 + * @param times - The corresponding timestamps for each row. 480 + * @param tempColumns - The names of the columns containing temperature data. 481 + * @returns An object with calculated temperature statistics. 482 + * @private 483 + */ 439 484 function calculateTemperatureStats(rows: Array<Record<string, number | string | null>>, times: Date[], tempColumns: string[]) { 440 485 // Gruppiere nach Tagen 441 486 const dayMap = new Map<string, { date: Date; maxTemp: number; minTemp: number; hasOver30: boolean; hasUnder0: boolean }>(); ··· 517 562 }; 518 563 } 519 564 565 + /** 566 + * Calculates rain statistics from the provided data. 567 + * This includes counting days with heavy rain (>30mm) and the total number of rain days. 568 + * @param rows - The data rows. 569 + * @param times - The corresponding timestamps for each row. 570 + * @param rainColumn - The name of the column containing rain data. 571 + * @returns An object with calculated rain statistics. 572 + * @private 573 + */ 520 574 function calculateRainStats(rows: Array<Record<string, number | string | null>>, times: Date[], rainColumn: string | null) { 521 575 if (!rainColumn) return { daysOver30mm: 0, totalDays: 0, totalPeriodDays: 0 }; 522 576 ··· 564 618 565 619 return { daysOver30mm, totalDays: rainDays, totalPeriodDays }; 566 620 } 567 - // Min/Max-Statistik für eine numerische Spalte (z.B. Wind/Böe) 621 + 622 + /** 623 + * Calculates the minimum and maximum values for a specific numeric column. 624 + * @param rows - The data rows. 625 + * @param times - The corresponding timestamps for each row. 626 + * @param column - The name of the column to analyze. 627 + * @returns An object containing the min and max values and their corresponding timestamps. 628 + * @private 629 + */ 568 630 function calculateMinMaxForColumn( 569 631 rows: Array<Record<string, number | string | null>>, 570 632 times: Date[], ··· 590 652 }; 591 653 } 592 654 655 + /** 656 + * Creates a formatter function for chart x-axis time ticks. 657 + * @param t0 - The baseline timestamp (in milliseconds) for the x-axis. 658 + * @param spanMin - The total time span in minutes (not currently used). 659 + * @param locale - The locale string (e.g., 'en-US', 'de'). 660 + * @returns A function that formats a minute offset into a date string. 661 + * @private 662 + */ 593 663 function makeTimeTickFormatter(t0: number, spanMin: number = 0, locale: string) { 594 664 return (v: number) => { 595 665 const d = new Date(t0 + Math.round(v) * 60000); ··· 603 673 }; 604 674 } 605 675 676 + /** 677 + * Creates a formatter function for the tooltip displayed on chart hover. 678 + * @param t0 - The baseline timestamp (in milliseconds) for the x-axis. 679 + * @param locale - The locale string (e.g., 'en-US', 'de'). 680 + * @returns A function that formats a minute offset into a full date-time string. 681 + * @private 682 + */ 606 683 function makeHoverTimeFormatter(t0: number, locale: string) { 607 684 return (v: number) => { 608 685 const d = new Date(t0 + Math.round(v) * 60000); ··· 610 687 }; 611 688 } 612 689 613 - // Locale-aware display formatter with safe fallback 690 + /** 691 + * Formats a Date object into a locale-aware string with a safe fallback. 692 + * @param d - The Date object to format. 693 + * @param locale - The locale string. 694 + * @returns The formatted date-time string. 695 + * @private 696 + */ 614 697 function formatDisplayLocale(d: Date, locale: string): string { 615 698 try { 616 699 return new Intl.DateTimeFormat(locale || 'de', { ··· 627 710 return `${dd}.${mm}.${yyyy} ${hh}:${mi}:${ss}`; 628 711 } 629 712 } 630 - // Locale-aware month name helper (1-12) 713 + 714 + /** 715 + * Gets the localized name of a month. 716 + * @param month - The month number (1-12). 717 + * @param locale - The locale string. 718 + * @returns The full month name. 719 + * @private 720 + */ 631 721 function getMonthName(month: number, locale: string): string { 632 722 const m = Math.max(1, Math.min(12, Math.floor(month))); 633 723 try { ··· 640 730 } 641 731 } 642 732 733 + /** 734 + * Defines the types of metrics available for channel sensors. 735 + * @private 736 + */ 643 737 type ChannelMetric = "Temperatur" | "Luftfeuchtigkeit" | "Taupunkt" | "Gefühlte Temperatur"; 644 738 739 + /** 740 + * Gets the internationalized display label for a given metric. 741 + * @param metric - The metric to get the label for. 742 + * @param t - The translation function. 743 + * @returns The translated display label. 744 + * @private 745 + */ 645 746 function metricDisplayLabel(metric: ChannelMetric, t: (key: string) => string): string { 646 747 const map: Record<string, string> = { 647 748 "Temperatur": t('fields.temperature'), ··· 652 753 return map[metric] || metric; 653 754 } 654 755 756 + /** 757 + * Defines the available datasets for fetching data. 758 + * `allsensors` includes all channel sensor data, while `main` is for the main weather station sensors. 759 + * @private 760 + */ 655 761 type Dataset = "allsensors" | "main"; 656 762 763 + /** 764 + * Defines the available data resolutions for API requests. 765 + * @private 766 + */ 657 767 type Resolution = "minute" | "hour" | "day"; 658 768 659 769 const COLORS = [ ··· 667 777 "#f97316", 668 778 ]; 669 779 780 + /** 781 + * The main component for the dashboard page. 782 + * It manages state for data fetching, user selections (like year, month, resolution), 783 + * and renders the appropriate charts based on user input. 784 + * 785 + * @returns The main dashboard component. 786 + */ 670 787 export default function Dashboard() { 671 788 const { t, i18n } = useTranslation(); 672 789 const locale = i18n.language || 'de'; ··· 1120 1237 ); 1121 1238 } 1122 1239 1240 + /** 1241 + * Renders a single line chart for a specific metric of a specific channel. 1242 + * @param data - The data for all sensors. 1243 + * @param chKey - The key of the channel to render (e.g., 'ch1'). 1244 + * @param metric - The metric to display. 1245 + * @param channelsCfg - The channel configuration. 1246 + * @param xBase - The baseline timestamp for the x-axis. 1247 + * @param t - The translation function. 1248 + * @param locale - The current locale. 1249 + * @returns A React component containing the line chart. 1250 + * @private 1251 + */ 1123 1252 function renderChannelChart(data: DataResp, chKey: string, metric: ChannelMetric, channelsCfg: ChannelsConfig, xBase: number | null, t: (key: string) => string, locale: string) { 1124 1253 const rows = data.rows || []; 1125 1254 if (!rows.length || !xBase) return <div className="text-xs text-gray-500">{t('statuses.noData')}</div>; ··· 1144 1273 ); 1145 1274 } 1146 1275 1276 + /** 1277 + * Renders all charts for the main weather station sensors. 1278 + * This includes temperature, rain, wind, and other metrics, along with statistical cards. 1279 + * @param data - The data from the 'main' dataset. 1280 + * @param xBase - The baseline timestamp for the x-axis. 1281 + * @param minuteData - Higher-resolution minute data for more accurate statistics. 1282 + * @param t - The translation function. 1283 + * @param locale - The current locale. 1284 + * @returns A React fragment containing all the charts for the main sensors. 1285 + * @private 1286 + */ 1147 1287 function renderMainCharts(data: DataResp, xBase: number | null, minuteData: DataResp | null, t: (key: string) => string, locale: string) { 1148 1288 const rows = data.rows || []; 1149 1289 if (!rows.length || !xBase) return <div className="text-xs text-gray-500">{t('statuses.noData')}</div>; ··· 1668 1808 ); 1669 1809 } 1670 1810 1671 - // Hilfsfunktion zum Identifizieren von Temperaturmetriken in den Hauptsensoren 1811 + /** 1812 + * Finds columns related to temperature from a list of headers. 1813 + * @param header - An array of header strings. 1814 + * @returns An array of header strings that are identified as temperature metrics. 1815 + * @private 1816 + */ 1672 1817 function findTemperatureColumns(header: string[]): string[] { 1673 1818 const tempColumns: string[] = []; 1674 1819 ··· 1686 1831 return tempColumns; 1687 1832 } 1688 1833 1689 - // Hilfsfunktion zum Identifizieren von Regenmetriken in den Hauptsensoren 1834 + /** 1835 + * Finds columns related to rain from a list of headers. 1836 + * It specifically looks for weekly, monthly, and yearly totals. 1837 + * @param header - An array of header strings. 1838 + * @returns An array of header strings identified as rain total metrics. 1839 + * @private 1840 + */ 1690 1841 function findRainColumns(header: string[]): string[] { 1691 1842 const rainColumns: string[] = []; 1692 1843 ··· 1731 1882 return rainColumns; 1732 1883 } 1733 1884 1734 - // Hilfsfunktion zum Identifizieren von Wind/Böe-Spalten in den Hauptsensoren 1885 + /** 1886 + * Finds columns for wind speed and wind gust from a list of headers. 1887 + * @param header - An array of header strings. 1888 + * @returns An object containing the identified wind and gust column names, or null if not found. 1889 + * @private 1890 + */ 1735 1891 function findWindColumns(header: string[]): { windCol: string | null; gustCol: string | null } { 1736 1892 let windCol: string | null = null; 1737 1893 let gustCol: string | null = null; ··· 1755 1911 return { windCol, gustCol }; 1756 1912 } 1757 1913 1758 - // Hilfsfunktion: Solarstrahlung und UV-Index identifizieren 1914 + /** 1915 + * Finds columns for solar radiation and UV index from a list of headers. 1916 + * @param header - An array of header strings. 1917 + * @returns An object containing the identified solar and UV column names, or null if not found. 1918 + * @private 1919 + */ 1759 1920 function findSolarUvColumns(header: string[]): { solarCol: string | null; uvCol: string | null } { 1760 1921 let solarCol: string | null = null; 1761 1922 let uvCol: string | null = null; ··· 1773 1934 return { solarCol, uvCol }; 1774 1935 } 1775 1936 1937 + /** 1938 + * Parses a string into a Date object. 1939 + * Handles multiple common date formats and provides a fallback. 1940 + * @param s - The date string to parse. 1941 + * @returns A Date object, or null if parsing fails. 1942 + * @private 1943 + */ 1776 1944 function toDate(s: string): Date | null { 1777 1945 // try YYYY/M/D H:MM 1778 1946 let m = s.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})\s+(\d{1,2}):(\d{1,2})/); ··· 1785 1953 return isNaN(d.getTime()) ? null : d; 1786 1954 } 1787 1955 1956 + /** 1957 + * Safely converts a value to a number, returning NaN for invalid inputs. 1958 + * @param v - The value to convert. 1959 + * @returns A number or NaN. 1960 + * @private 1961 + */ 1788 1962 function numOrNaN(v: any): number { 1789 1963 if (v == null) return NaN; 1790 1964 const n = typeof v === "number" ? v : Number(v); 1791 1965 return isNaN(n) ? NaN : n; 1792 1966 } 1793 1967 1794 - // Helpers for time range controls 1968 + /** 1969 + * Clamps a number between a minimum and maximum value. 1970 + * @param n - The number to clamp. 1971 + * @param min - The minimum value. 1972 + * @param max - The maximum value. 1973 + * @returns The clamped number. 1974 + * @private 1975 + */ 1795 1976 function clamp(n: number, min: number, max: number) { return Math.max(min, Math.min(max, n)); } 1977 + 1978 + /** 1979 + * Finds the index of the nearest time in an array of dates to a given timestamp. 1980 + * @param times - A sorted array of Date objects. 1981 + * @param ms - The timestamp in milliseconds to find the nearest match for. 1982 + * @returns The index of the nearest time. 1983 + * @private 1984 + */ 1796 1985 function nearestIndex(times: Date[], ms: number) { 1797 1986 if (!times.length) return 0; 1798 1987 let lo = 0, hi = times.length - 1; ··· 1808 1997 const d1 = Math.abs(times[i1].getTime() - ms); 1809 1998 return d0 < d1 ? i0 : i1; 1810 1999 } 2000 + 2001 + /** 2002 + * Formats a Date object for display in `DD.MM.YYYY HH:MI` format. 2003 + * @param d - The Date object. 2004 + * @returns The formatted string. 2005 + * @private 2006 + */ 1811 2007 function formatDisplay(d: Date) { 1812 2008 const dd = pad2(d.getDate()); 1813 2009 const mm = pad2(d.getMonth() + 1); ··· 1816 2012 const mi = pad2(d.getMinutes()); 1817 2013 return `${dd}.${mm}.${yyyy} ${hh}:${mi}`; 1818 2014 } 2015 + 2016 + /** 2017 + * Formats a Date object into a string suitable for `datetime-local` input fields (`YYYY-MM-DDTHH:MI`). 2018 + * @param d - The Date object. 2019 + * @returns The formatted string. 2020 + * @private 2021 + */ 1819 2022 function formatLocal(d: Date) { 1820 2023 const yyyy = d.getFullYear(); 1821 2024 const mm = pad2(d.getMonth() + 1); ··· 1824 2027 const mi = pad2(d.getMinutes()); 1825 2028 return `${yyyy}-${mm}-${dd}T${hh}:${mi}`; 1826 2029 } 2030 + 2031 + /** 2032 + * Formats a Date object into a string suitable for API requests (`YYYY-MM-DDTHH:MI`). 2033 + * @param d - The Date object. 2034 + * @returns The formatted string. 2035 + * @private 2036 + */ 1827 2037 function formatForApi(d: Date) { 1828 2038 const yyyy = d.getFullYear(); 1829 2039 const mm = pad2(d.getMonth() + 1); ··· 1833 2043 return `${yyyy}-${mm}-${dd}T${hh}:${mi}`; 1834 2044 } 1835 2045 2046 + /** 2047 + * Generates a pretty label for a sensor header, replacing 'CHx' with its configured name. 2048 + * @param header - The raw header string (e.g., "CH1 Temperatur"). 2049 + * @param cfg - The channels configuration. 2050 + * @returns A user-friendly label (e.g., "Living Room Temperatur"). 2051 + * @private 2052 + */ 1836 2053 function prettyAllsensorsLabel(header: string, cfg: ChannelsConfig) { 1837 2054 // Replace leading CHx with configured channel name if present 1838 2055 const m = header.match(/^CH(\d+)\s+(.*)$/); ··· 1844 2061 return header; 1845 2062 } 1846 2063 2064 + /** 2065 + * Gets the configured name for a channel key. 2066 + * @param key - The channel key (e.g., 'ch1'). 2067 + * @param cfg - The channels configuration. 2068 + * @returns The configured name or the key itself as a fallback. 2069 + * @private 2070 + */ 1847 2071 function channelName(key: string, cfg: ChannelsConfig) { 1848 2072 const c = cfg[key]; 1849 2073 if (!c) return key.toUpperCase(); 1850 2074 return c.name || key.toUpperCase(); 1851 2075 } 1852 2076 2077 + /** 2078 + * Gets a sorted list of channel keys from the configuration. 2079 + * Provides a default list if the configuration is empty. 2080 + * @param cfg - The channels configuration. 2081 + * @returns A sorted array of channel keys. 2082 + * @private 2083 + */ 1853 2084 function getChannelKeys(cfg: ChannelsConfig): string[] { 1854 2085 const keys = Object.keys(cfg); 1855 2086 if (keys.length) return keys.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); 1856 2087 return ["ch1","ch2","ch3","ch4","ch5","ch6","ch7","ch8"]; 1857 2088 } 1858 2089 2090 + /** 2091 + * Finds the correct header key for a given metric and channel number in the 'allsensors' dataset. 2092 + * It handles synonyms for metric names. 2093 + * @param header - The array of header strings from the data. 2094 + * @param metric - The metric to find (e.g., "Temperatur"). 2095 + * @param chNum - The channel number as a string (e.g., "1"). 2096 + * @returns The matching header key, or a fallback. 2097 + * @private 2098 + */ 1859 2099 function headerKeyForAllsensors(header: string[], metric: string, chNum: string): string { 1860 2100 // Prefer CHx <metric> 1861 2101 const synonyms: Record<string, string[]> = { ··· 1884 2124 return header[1] || ""; 1885 2125 } 1886 2126 2127 + /** 2128 + * Infers which columns in a dataset are numeric based on a sample of rows. 2129 + * @param data - The dataset to analyze. 2130 + * @returns An array of header strings that are determined to be numeric. 2131 + * @private 2132 + */ 1887 2133 function inferNumericColumns(data: DataResp | null): string[] { 1888 2134 if (!data) return []; 1889 2135 const header = data.header || []; ··· 1902 2148 return numeric; 1903 2149 } 1904 2150 1905 - // Units helpers 2151 + /** 2152 + * Returns the appropriate unit for a given channel metric. 2153 + * @param metric - The channel metric. 2154 + * @returns The unit string (e.g., "°C", "%"). 2155 + * @private 2156 + */ 1906 2157 function unitForMetric(metric: ChannelMetric): string { 1907 2158 switch (metric) { 1908 2159 case "Temperatur": ··· 1916 2167 } 1917 2168 } 1918 2169 2170 + /** 2171 + * Infers the measurement unit from a header string. 2172 + * @param header - The header string (e.g., "Temperatur Aussen"). 2173 + * @returns The inferred unit string (e.g., "°C", "mm", "km/h"). 2174 + * @private 2175 + */ 1919 2176 function unitForHeader(header: string): string { 1920 2177 const s = header.toLowerCase(); 1921 2178 // Rain
+126 -8
src/components/Gauges.tsx
··· 6 6 import { useRealtime } from "@/contexts/RealtimeContext"; 7 7 import { computeAstro, formatTime } from "@/lib/astro"; 8 8 9 - // Lightweight helpers – duplicated to keep this component self-contained 9 + /** 10 + * Safely reads a nested property from an object. 11 + * @param obj - The object to read from. 12 + * @param path - The dot-separated path to the property. 13 + * @returns The property value, or undefined if not found. 14 + * @private 15 + */ 10 16 function tryRead(obj: any, path: string): any { 11 17 return path.split(".").reduce((acc, key) => (acc && key in acc ? (acc as any)[key] : undefined), obj); 12 18 } 13 19 14 - // Temperature/Humidity color helpers shared across components 20 + /** 21 + * Determines the color for a given temperature value based on a predefined scale. 22 + * @param t - The temperature in Celsius. 23 + * @returns A hex color string. 24 + * @private 25 + */ 15 26 function tempColor(t: number | null): string { 16 27 // Reversed spectrum: violet at cold end, red at hot end 17 28 // Red at +45°C, violet around -20°C (and below). Thresholds in °C (converted from the given Fahrenheit table). ··· 33 44 return "#b91c1c"; // red-700 above +45°C 34 45 } 35 46 47 + /** 48 + * Determines the color for a given humidity value based on a predefined scale. 49 + * @param h - The humidity in percent. 50 + * @returns A hex color string. 51 + * @private 52 + */ 36 53 function humColor(h: number | null): string { 37 54 // Similar reversed spectrum from 0% (violet) to 100% (red) 38 55 if (h == null || !isFinite(h)) return "#94a3b8"; ··· 49 66 return "#dc2626"; // red-600 to 100% 50 67 } 51 68 52 - // Vertical temperature gradient bar with ticks every `step` degrees 69 + /** 70 + * Renders a vertical temperature gradient bar with tick marks. 71 + * @param props - The component props. 72 + * @param props.min - The minimum temperature of the scale. 73 + * @param props.max - The maximum temperature of the scale. 74 + * @param props.step - The interval for tick marks. 75 + * @param props.height - The height of the SVG element. 76 + * @param props.width - The width of the bar itself. 77 + * @returns A React SVG component. 78 + * @private 79 + */ 53 80 function TempGradientBar(props: { min: number; max: number; step: number; height?: number; width?: number }) { 54 81 const { min, max, step, height = 200, width = 28 } = props; 55 82 const pad = 12; ··· 111 138 ); 112 139 } 113 140 114 - // Vertical humidity gradient bar (1–100%) with hard edges and labels 141 + /** 142 + * Renders a vertical humidity gradient bar with tick marks. 143 + * @param props - The component props. 144 + * @param props.min - The minimum humidity of the scale. 145 + * @param props.max - The maximum humidity of the scale. 146 + * @param props.step - The interval for tick marks. 147 + * @param props.height - The height of the SVG element. 148 + * @param props.width - The width of the bar itself. 149 + * @returns A React SVG component. 150 + * @private 151 + */ 115 152 function HumGradientBar(props: { min: number; max: number; step: number; height?: number; width?: number }) { 116 153 const { min, max, step, height = 200, width = 28 } = props; 117 154 const pad = 12; ··· 169 206 ); 170 207 } 171 208 209 + /** 210 + * Safely extracts a numeric value from various possible data structures. 211 + * @param v - The value to parse, which can be a number, string, or object with a `value` property. 212 + * @returns The numeric value, or null if parsing fails. 213 + * @private 214 + */ 172 215 function numVal(v: any): number | null { 173 216 if (v == null) return null; 174 217 if (typeof v === "number") return Number.isFinite(v) ? v : null; ··· 182 225 return null; 183 226 } 184 227 228 + /** 229 + * Extracts a value and its unit from a potential value-unit object. 230 + * @param v - The value, which can be a primitive or an object like `{ value: 10, unit: '°C' }`. 231 + * @returns An object containing the value and an optional unit. 232 + * @private 233 + */ 185 234 function valueAndUnit(v: any): { value: string | number | null; unit?: string } { 186 235 if (v == null) return { value: null }; 187 236 if (typeof v === "object" && ("value" in v)) { ··· 190 239 return { value: v }; 191 240 } 192 241 242 + /** 243 + * Formats a value-unit object into a display string. 244 + * @param vu - The value-unit object. 245 + * @param fallbackUnit - A fallback unit to use if the object doesn't specify one. 246 + * @returns A formatted string like "10 °C" or "—" if the value is null. 247 + * @private 248 + */ 193 249 function fmtVU(vu: { value: string | number | null; unit?: string }, fallbackUnit?: string) { 194 250 if (vu.value == null || vu.value === "") return "—"; 195 251 const unit = vu.unit ?? fallbackUnit ?? ""; 196 252 return `${vu.value}${unit ? ` ${unit}` : ""}`; 197 253 } 198 254 199 - // Derived metrics 255 + /** 256 + * Calculates the dew point. 257 + * @param temperature - The temperature in Celsius. 258 + * @param humidity - The relative humidity in percent. 259 + * @returns The calculated dew point in Celsius. 260 + * @private 261 + */ 200 262 function calculateDewPoint(temperature: number, humidity: number): number { 201 263 const a = 17.27; 202 264 const b = 237.7; ··· 205 267 return Number.isFinite(dewPoint) ? Math.round(dewPoint * 10) / 10 : temperature; 206 268 } 207 269 270 + /** 271 + * Calculates the heat index (apparent temperature). 272 + * @param temperature - The temperature in Celsius. 273 + * @param humidity - The relative humidity in percent. 274 + * @returns The calculated heat index in Celsius. 275 + * @private 276 + */ 208 277 function calculateHeatIndex(temperature: number, humidity: number): number { 209 278 if (temperature < 20) return temperature; 210 279 const t = temperature; ··· 223 292 return Number.isFinite(hi) ? Math.round(hi * 10) / 10 : temperature; 224 293 } 225 294 226 - // Min/Max display component for temperature gauges 295 + /** 296 + * A component that displays the minimum and maximum values for a sensor, 297 + * typically overlaid on a gauge. 298 + * @param props - The component props. 299 + * @param props.sensorKey - The key identifying the sensor in the min/max data object. 300 + * @param props.tempMinMax - The object containing min/max data for all sensors. 301 + * @param props.unit - The unit to display (e.g., "°C"). 302 + * @param props.isHumidity - If true, it uses the humidity min/max data instead of temperature. 303 + * @returns A React component showing min/max values with timestamps. 304 + * @private 305 + */ 227 306 function MinMaxDisplay(props: { 228 307 sensorKey: string; 229 308 tempMinMax: any; ··· 270 349 ); 271 350 } 272 351 273 - // Enhanced donut gauge with tick labels, segments and extra rings 352 + /** 353 + * A highly customizable donut gauge component for displaying sensor values. 354 + * It supports ticks, labels, color segments, and overlaying multiple data rings. 355 + * @param props - The component props. 356 + * @returns A React SVG component representing the gauge. 357 + * @private 358 + */ 274 359 function DonutGauge(props: { 275 360 label: string; 276 361 value: number | null; ··· 467 552 ); 468 553 } 469 554 555 + /** 556 + * A simple Key Performance Indicator (KPI) display box. 557 + * @param props - The component props. 558 + * @param props.label - The label for the KPI. 559 + * @param props.value - The value of the KPI. 560 + * @returns A React component for displaying a KPI. 561 + * @private 562 + */ 470 563 function KPI(props: { label: string; value: React.ReactNode }) { 471 564 return ( 472 565 <div className="p-3 rounded border border-gray-200 dark:border-neutral-800"> ··· 476 569 ); 477 570 } 478 571 479 - // Raindrop icon with fill level based on hourly rate 572 + /** 573 + * Renders a raindrop icon with a fill level corresponding to the rain rate. 574 + * @param props - The component props. 575 + * @param props.rate - The rain rate. 576 + * @param props.unit - The unit of the rain rate. 577 + * @param props.size - The size of the SVG icon. 578 + * @returns A React SVG component of a raindrop. 579 + * @private 580 + */ 480 581 function Raindrop({ rate, unit = "mm/hr", size = 84 }: { rate: number | null; unit?: string; size?: number }) { 481 582 const id = React.useMemo(() => `drop-${Math.random().toString(36).slice(2)}` , []); 482 583 let v = rate ?? 0; ··· 523 624 ); 524 625 } 525 626 627 + /** 628 + * Renders a compass for displaying wind direction and speed. 629 + * @param props - The component props. 630 + * @param props.dir - The wind direction in degrees. 631 + * @param props.speed - The wind speed. 632 + * @param props.gust - The wind gust speed. 633 + * @param props.unit - The unit for speed and gust. 634 + * @returns A React component showing a wind compass. 635 + * @private 636 + */ 526 637 function CompassWind(props: { dir: number | null; speed: number | null; gust?: number | null; unit?: string }) { 527 638 const { dir, speed, gust, unit = "" } = props; 528 639 const { t } = useTranslation(); ··· 581 692 ); 582 693 } 583 694 695 + /** 696 + * The main component that renders all the gauges and KPIs for the dashboard. 697 + * It fetches real-time data, channel configurations, device info, and min/max temperature data, 698 + * and then displays it using various gauge and chart components. 699 + * 700 + * @returns The main Gauges component. 701 + */ 584 702 export default function Gauges() { 585 703 const { t, i18n } = useTranslation(); 586 704 const { data, error, loading, lastUpdated } = useRealtime();
+8
src/components/I18nProvider.tsx
··· 4 4 import { I18nextProvider } from "react-i18next"; 5 5 import i18n from "@/lib/i18n"; 6 6 7 + /** 8 + * Provides the i18next context to its children and manages the application's language settings. 9 + * It initializes the language from localStorage or the browser's settings and updates the `<html>` tag's `lang` attribute. 10 + * 11 + * @param props - The component props. 12 + * @param props.children - The child components to render within the provider. 13 + * @returns A React component that provides i18next context. 14 + */ 7 15 export default function I18nProvider({ children }: { children: React.ReactNode }) { 8 16 // Initialize language preference from localStorage or browser 9 17 useEffect(() => {
+6
src/components/LanguageSwitcher.tsx
··· 3 3 import React from "react"; 4 4 import { useTranslation } from "react-i18next"; 5 5 6 + /** 7 + * A component that allows the user to switch between supported languages (DE and EN). 8 + * It highlights the currently active language and handles the language change logic. 9 + * 10 + * @returns A React component with language switching buttons. 11 + */ 6 12 export default function LanguageSwitcher() { 7 13 const { i18n } = useTranslation(); 8 14 const cur = i18n.language || "de";
+30 -7
src/components/LineChart.tsx
··· 2 2 3 3 import React, { useMemo, useState } from "react"; 4 4 5 + /** 6 + * Represents a single point in a line chart series. 7 + */ 5 8 export type LinePoint = { x: number; y: number; label?: string }; 9 + 10 + /** 11 + * Represents a series of data for the line chart. 12 + */ 6 13 export type LineSeries = { id: string; color: string; points: LinePoint[] }; 7 14 15 + /** 16 + * Props for the LineChart component. 17 + */ 8 18 type Props = { 19 + /** The array of data series to plot. */ 9 20 series: LineSeries[]; 21 + /** The height of the chart canvas. */ 10 22 height?: number; 23 + /** The label for the Y-axis. */ 11 24 yLabel?: string; 25 + /** A function to format the X-axis tick labels. */ 12 26 xTickFormatter?: (v: number) => string; 13 - // Optional dedicated formatter for hover time (overrides xTickFormatter in tooltip) 27 + /** A dedicated formatter for the time displayed in the hover tooltip. */ 14 28 hoverTimeFormatter?: (v: number) => string; 29 + /** The label for the X-axis. */ 15 30 xLabel?: string; 31 + /** Whether to display the legend. */ 16 32 showLegend?: boolean; 17 - // Optional: render as vertical bars (useful for daily rainfall) 33 + /** If true, renders the data as vertical bars instead of a line. */ 18 34 bars?: boolean; 19 - // Bar width in x-units (minutes); defaults to spanX/(tickCount*1.5) 35 + /** The width of bars in data units (e.g., minutes). */ 20 36 barWidth?: number; 21 - // Optional fixed bar width in pixels (overrides barWidth) 37 + /** A fixed width for bars in pixels, overriding `barWidth`. */ 22 38 barWidthPx?: number; 23 - // Show hover crosshair and current values tooltip 39 + /** If true, shows a crosshair and tooltip on hover. */ 24 40 showHover?: boolean; 25 - // Optional unit appended to hover value, e.g. "mm" 41 + /** The unit to append to the Y-value in the tooltip. */ 26 42 yUnit?: string; 27 - // Optional custom formatter for hover value 43 + /** A custom function to format the value displayed in the tooltip. */ 28 44 valueFormatter?: (v: number) => string; 29 45 }; 30 46 47 + /** 48 + * A responsive line chart component built with SVG. 49 + * It supports multiple series, tooltips, legends, and rendering as bar charts. 50 + * 51 + * @param props - The component props. 52 + * @returns A React component that renders the line chart. 53 + */ 31 54 export default function LineChart({ series, height = 220, yLabel, xTickFormatter, hoverTimeFormatter, xLabel, showLegend = true, bars = false, barWidth, barWidthPx, showHover = true, yUnit, valueFormatter }: Props) { 32 55 const padding = { top: 20, right: 12, bottom: 28, left: 36 }; 33 56 const width = 800; // SVG viewBox width; scales responsively via CSS
+32 -7
src/components/LineChartChartJS.tsx
··· 33 33 CategoryScale, 34 34 ); 35 35 36 + /** 37 + * Represents a single point in a line chart series. 38 + * This type is compatible with the other LineChart component. 39 + */ 36 40 export type LinePoint = { x: number; y: number; label?: string }; 41 + 42 + /** 43 + * Represents a series of data for the line chart. 44 + * This type is compatible with the other LineChart component. 45 + */ 37 46 export type LineSeries = { id: string; color: string; points: LinePoint[] }; 38 47 48 + /** 49 + * Props for the LineChart component, powered by Chart.js. 50 + */ 39 51 type Props = { 52 + /** The array of data series to plot. */ 40 53 series: LineSeries[]; 54 + /** The height of the chart canvas. */ 41 55 height?: number; 56 + /** The label for the Y-axis. */ 42 57 yLabel?: string; 58 + /** A function to format the X-axis tick labels. */ 43 59 xTickFormatter?: (v: number) => string; 44 - // Optional dedicated formatter for hover time (overrides xTickFormatter in tooltip) 60 + /** A dedicated formatter for the time displayed in the hover tooltip. */ 45 61 hoverTimeFormatter?: (v: number) => string; 62 + /** The label for the X-axis. */ 46 63 xLabel?: string; 64 + /** Whether to display the legend. */ 47 65 showLegend?: boolean; 48 - // Optional: render as vertical bars (useful for daily rainfall) 66 + /** If true, renders the data as vertical bars instead of a line. */ 49 67 bars?: boolean; 50 - // Bar width in x-units (minutes); not used directly by Chart.js but kept for API compatibility 68 + /** The width of bars in data units (kept for API compatibility, not used by Chart.js). */ 51 69 barWidth?: number; 52 - // Optional fixed bar width in pixels 70 + /** A fixed width for bars in pixels. */ 53 71 barWidthPx?: number; 54 - // Show hover crosshair (handled by tooltip in Chart.js) 72 + /** If true, shows a tooltip on hover. */ 55 73 showHover?: boolean; 56 - // Optional unit appended to hover value, e.g. "mm" 74 + /** The unit to append to the Y-value in the tooltip. */ 57 75 yUnit?: string; 58 - // Optional custom formatter for hover value 76 + /** A custom function to format the value displayed in the tooltip. */ 59 77 valueFormatter?: (v: number) => string; 60 78 }; 61 79 80 + /** 81 + * A responsive line or bar chart component powered by Chart.js. 82 + * It supports multiple series, tooltips, legends, zooming, and panning. 83 + * 84 + * @param props - The component props. 85 + * @returns A React component that renders the chart. 86 + */ 62 87 export default function LineChart({ 63 88 series, 64 89 height = 220,
+87
src/components/Realtime.tsx
··· 9 9 10 10 type RTData = any; 11 11 12 + /** 13 + * A simple component to display a label and a value side-by-side. 14 + * @param props - The component props. 15 + * @param props.label - The label to display. 16 + * @param props.value - The value to display. 17 + * @returns A React component with a label and value. 18 + * @private 19 + */ 12 20 function LabelValue({ label, value }: { label: string; value: React.ReactNode }) { 13 21 return ( 14 22 <div className="flex items-center justify-between py-1 text-sm"> ··· 18 26 ); 19 27 } 20 28 29 + /** 30 + * A component to display a temperature value along with its daily min/max values. 31 + * @param props - The component props. 32 + * @returns A React component for displaying temperature with min/max data. 33 + * @private 34 + */ 21 35 function TemperatureLabelValue({ 22 36 label, 23 37 currentTemp, ··· 66 80 ); 67 81 } 68 82 83 + /** 84 + * A component to display a humidity value along with its daily min/max values. 85 + * @param props - The component props. 86 + * @returns A React component for displaying humidity with min/max data. 87 + * @private 88 + */ 69 89 function HumidityLabelValue({ 70 90 label, 71 91 currentHumidity, ··· 114 134 ); 115 135 } 116 136 137 + /** 138 + * Displays the current value of a sensor along with its daily minimum and maximum. 139 + * @param props - The component props. 140 + * @returns A React component showing the current value and min/max stats. 141 + * @private 142 + */ 117 143 function MinMaxDisplay({ 118 144 current, 119 145 minMax, ··· 153 179 ); 154 180 } 155 181 182 + /** 183 + * Safely reads a nested property from an object. 184 + * @param obj - The object to read from. 185 + * @param path - The dot-separated path to the property. 186 + * @returns The property value, or undefined if not found. 187 + * @private 188 + */ 156 189 function tryRead(obj: any, path: string): any { 157 190 return path.split(".").reduce((acc, key) => (acc && key in acc ? acc[key] : undefined), obj); 158 191 } 159 192 193 + /** 194 + * Safely extracts a numeric value from various possible data structures. 195 + * @param v - The value to parse. 196 + * @returns The numeric value, or null if parsing fails. 197 + * @private 198 + */ 160 199 function numVal(v: any): number | null { 161 200 if (v == null) return null; 162 201 if (typeof v === "number") return Number.isFinite(v) ? v : null; ··· 170 209 return null; 171 210 } 172 211 212 + /** 213 + * Calculates the dew point. 214 + * @param temperature - The temperature in Celsius. 215 + * @param humidity - The relative humidity in percent. 216 + * @returns The calculated dew point in Celsius. 217 + * @private 218 + */ 173 219 function calculateDewPoint(temperature: number, humidity: number): number { 174 220 // Magnus-Formel für Taupunktberechnung 175 221 const a = 17.27; ··· 181 227 return Number.isFinite(dewPoint) ? Math.round(dewPoint * 10) / 10 : temperature; 182 228 } 183 229 230 + /** 231 + * Calculates the heat index (apparent temperature). 232 + * @param temperature - The temperature in Celsius. 233 + * @param humidity - The relative humidity in percent. 234 + * @returns The calculated heat index in Celsius. 235 + * @private 236 + */ 184 237 function calculateHeatIndex(temperature: number, humidity: number): number { 185 238 // Vereinfachte Formel für den Wärmeindex (Heat Index) 186 239 if (temperature < 20) { ··· 209 262 return Number.isFinite(heatIndex) ? Math.round(heatIndex * 10) / 10 : temperature; 210 263 } 211 264 265 + /** 266 + * Extracts a value and its unit from a potential value-unit object. 267 + * @param v - The value object. 268 + * @returns An object containing the value and an optional unit. 269 + * @private 270 + */ 212 271 function valueAndUnit(v: any): { value: string | number | null; unit?: string } { 213 272 if (v == null) return { value: null }; 214 273 if (typeof v === "object" && ("value" in v)) { ··· 217 276 return { value: v }; 218 277 } 219 278 279 + /** 280 + * Formats a value-unit object into a display string. 281 + * @param vu - The value-unit object. 282 + * @param fallbackUnit - A fallback unit to use if the object doesn't specify one. 283 + * @returns A formatted string like "10 °C" or "—" if the value is null. 284 + * @private 285 + */ 220 286 function fmtVU(vu: { value: string | number | null; unit?: string }, fallbackUnit?: string) { 221 287 if (vu.value == null || vu.value === "") return "—"; 222 288 const unit = vu.unit ?? fallbackUnit ?? ""; 223 289 return `${vu.value}${unit ? ` ${unit}` : ""}`; 224 290 } 225 291 292 + /** 293 + * Formats a battery status value into a localized string ("OK" or "Low"). 294 + * @param v - The battery status value. 295 + * @param t - The translation function. 296 + * @returns The localized battery status. 297 + * @private 298 + */ 226 299 function fmtBattery(v: any, t: (key: string) => string) { 227 300 const vu = valueAndUnit(v); 228 301 if (vu.value == null || vu.value === "") return "—"; ··· 231 304 return n === 0 ? t('statuses.ok') : t('statuses.low'); 232 305 } 233 306 307 + /** 308 + * Gets a translated, human-readable label for a given data key. 309 + * @param key - The data key (e.g., "wind_speed"). 310 + * @param t - The translation function. 311 + * @returns The internationalized label. 312 + * @private 313 + */ 234 314 function i18nLabel(key: string, t: (key: string) => string): string { 235 315 const k = key.toLowerCase(); 236 316 const map: Record<string, string> = { ··· 250 330 return map[k] || key.replace(/_/g, " "); 251 331 } 252 332 333 + /** 334 + * The main component for displaying real-time weather data in a list format. 335 + * It fetches and displays current conditions, sensor data, astronomical information, 336 + * and battery statuses. 337 + * 338 + * @returns A React component that renders the real-time data view. 339 + */ 253 340 export default function Realtime() { 254 341 const { t, i18n } = useTranslation(); 255 342 const { data, error, loading, lastUpdated } = useRealtime();
+12 -1
src/constants.js
··· 1 - // API Endpoints 1 + /** 2 + * A collection of API endpoint paths used throughout the application. 3 + * @property {string} RT_LAST - Endpoint for the last received real-time data. 4 + * @property {string} CONFIG_CHANNELS - Endpoint for channel configuration. 5 + * @property {string} DEVICE_INFO - Endpoint for device information (timezone, coordinates). 6 + * @property {string} TEMP_MINMAX - Endpoint to get today's min/max temperature data. 7 + * @property {string} TEMP_MINMAX_UPDATE - Endpoint to trigger an update of min/max data. 8 + * @property {string} DATA_MONTHS - Endpoint to get the list of available months with data. 9 + * @property {string} DATA_EXTENT - Endpoint to get the global time range of all data. 10 + * @property {string} DATA_ALLSENSORS - Endpoint for historical data from all channel sensors. 11 + * @property {string} DATA_MAIN - Endpoint for historical data from the main weather station sensors. 12 + */ 2 13 export const API_ENDPOINTS = { 3 14 // Realtime data 4 15 RT_LAST: '/api/rt/last',
+20
src/contexts/RealtimeContext.tsx
··· 5 5 6 6 type RTData = any; 7 7 8 + /** 9 + * Defines the shape of the RealtimeContext. 10 + */ 8 11 interface RealtimeContextType { 12 + /** The most recent real-time data object. */ 9 13 data: RTData | null; 14 + /** Any error message from the last fetch attempt. */ 10 15 error: string | null; 16 + /** True if data is currently being fetched. */ 11 17 loading: boolean; 18 + /** The timestamp of the last successful data update. */ 12 19 lastUpdated: Date | null; 20 + /** A function to manually trigger a data refetch. */ 13 21 refetch: () => void; 14 22 } 15 23 16 24 const RealtimeContext = createContext<RealtimeContextType | undefined>(undefined); 17 25 26 + /** 27 + * A React context provider that fetches and manages real-time weather data. 28 + * It periodically refetches the data and makes it available to all child components. 29 + * @param props - The component props. 30 + * @param props.children - The child components that will consume the context. 31 + * @returns A RealtimeContext.Provider component. 32 + */ 18 33 export function RealtimeProvider({ children }: { children: React.ReactNode }) { 19 34 const [data, setData] = useState<RTData | null>(null); 20 35 const [error, setError] = useState<string | null>(null); ··· 70 85 ); 71 86 } 72 87 88 + /** 89 + * A custom hook to access the real-time weather data context. 90 + * Throws an error if used outside of a `RealtimeProvider`. 91 + * @returns The real-time context, including data, loading state, errors, and a refetch function. 92 + */ 73 93 export function useRealtime() { 74 94 const context = useContext(RealtimeContext); 75 95 if (context === undefined) {
+9
src/instrumentation.ts
··· 6 6 var __rtPoller: NodeJS.Timer | undefined; 7 7 } 8 8 9 + /** 10 + * This function is registered to run when the Next.js server starts. 11 + * It sets up a background poller to periodically fetch real-time data from the weather station 12 + * and archive it. This ensures that the latest data is always available in a cache, 13 + * even if a user has not recently visited the site. 14 + * 15 + * It runs only on the Node.js runtime, not on the Edge runtime. 16 + * A global variable is used to prevent multiple pollers from running in development due to HMR. 17 + */ 9 18 export async function register() { 10 19 // Only run on Node.js runtime (not Edge) 11 20 if (process.env.NEXT_RUNTIME === "edge") return;
+44 -1
src/lib/astro.ts
··· 1 1 import SunCalc from "suncalc"; 2 2 3 + /** 4 + * Represents the result of astronomical calculations. 5 + * @property {Date | null} sunrise - The time of sunrise. 6 + * @property {Date | null} sunset - The time of sunset. 7 + * @property {Date | null} moonrise - The time of moonrise. 8 + * @property {Date | null} moonset - The time of moonset. 9 + * @property {number} phase - The moon phase, from 0.0 (new moon) to 1.0 (new moon). 10 + * @property {string} phaseName - The name of the moon phase. 11 + * @property {number} illumination - The fraction of the moon's illuminated limb. 12 + * @property {Date | null} civilDawn - The time when the sun is 6 degrees below the horizon in the morning. 13 + * @property {Date | null} civilDusk - The time when the sun is 6 degrees below the horizon in the evening. 14 + * @property {Date | null} nauticalDawn - The time when the sun is 12 degrees below the horizon in the morning. 15 + * @property {Date | null} nauticalDusk - The time when the sun is 12 degrees below the horizon in the evening. 16 + * @property {Date | null} astronomicalDawn - The time when the sun is 18 degrees below the horizon in the morning. 17 + * @property {Date | null} astronomicalDusk - The time when the sun is 18 degrees below the horizon in the evening. 18 + */ 3 19 export type AstroResult = { 4 20 sunrise: Date | null; 5 21 sunset: Date | null; ··· 17 33 astronomicalDusk: Date | null;// Sun -18° -> end of astronomical twilight (night begins) 18 34 }; 19 35 36 + /** 37 + * Gets the name of the moon phase for a given phase value. 38 + * @param {number} phase - The moon phase, from 0.0 (new moon) to 1.0 (new moon). 39 + * @param {string} [locale="en"] - The locale to use for the phase name (e.g., "en" or "de"). 40 + * @returns {string} The name of the moon phase. 41 + */ 20 42 export function moonPhaseName(phase: number, locale: string = "en"): string { 21 - // 0 new, 0.25 first quarter, 0.5 full, 0.75 last quarter 43 + // Phase: 0=new, 0.25=first quarter, 0.5=full, 0.75=last quarter 22 44 const namesEn = [ 23 45 "New Moon", 24 46 "Waxing Crescent", ··· 43 65 return (locale?.startsWith("de") ? namesDe : namesEn)[idx]; 44 66 } 45 67 68 + /** 69 + * Computes astronomical data for a given latitude, longitude, and date. 70 + * @param {number} lat - The latitude. 71 + * @param {number} lon - The longitude. 72 + * @param {Date} [date=new Date()] - The date for the calculation. 73 + * @param {string} [locale="en"] - The locale for the moon phase name. 74 + * @returns {AstroResult} An object containing the astronomical data. 75 + */ 46 76 export function computeAstro(lat: number, lon: number, date: Date = new Date(), locale: string = "en"): AstroResult { 47 77 const times = SunCalc.getTimes(date, lat, lon); 48 78 const mt = SunCalc.getMoonTimes(date, lat, lon, true /* UTC to avoid host tz issues */); ··· 65 95 }; 66 96 } 67 97 98 + /** 99 + * Formats a date object into a time string (HH:mm). 100 + * @param {Date | null} d - The date to format. 101 + * @param {string} [tz] - The time zone to use. 102 + * @param {string} [locale="en"] - The locale to use for formatting. 103 + * @returns {string} The formatted time string, or "—" if the date is null. 104 + */ 68 105 export function formatTime(d: Date | null, tz?: string, locale: string = "en"): string { 69 106 if (!d) return "—"; 70 107 try { ··· 81 118 } 82 119 } 83 120 121 + /** 122 + * Calculates the percentage of the day that has passed for a given date and time zone. 123 + * @param {Date} d - The date object. 124 + * @param {string} [tz] - The time zone to use. 125 + * @returns {number} The percentage of the day passed, from 0.0 to 1.0. 126 + */ 84 127 export function timeOfDayPercent(d: Date, tz?: string): number { 85 128 // returns 0..1 position within day for the given date in tz 86 129 try {
+27
src/lib/csv.ts
··· 2 2 import path from "path"; 3 3 import { parseTimestamp, floorToResolution, keyForResolution, type Resolution } from "@/lib/time"; 4 4 5 + /** 6 + * Represents a row of data from a CSV file. 7 + * The `time` property is always a string, while other properties can be strings, numbers, or null. 8 + */ 5 9 export type Row = { [key: string]: string | number | null } & { time: string }; 6 10 11 + /** 12 + * Reads a CSV file from a relative path. 13 + * @param {string} relPath - The relative path to the CSV file. 14 + * @returns {Promise<string>} A promise that resolves with the content of the file as a string. 15 + */ 7 16 export async function readCsvFile(relPath: string): Promise<string> { 8 17 const base = process.cwd(); 9 18 const abs = path.join(base, relPath); 10 19 return fs.readFile(abs, "utf8"); 11 20 } 12 21 22 + /** 23 + * Parses a CSV string into a header array and an array of row objects. 24 + * @param {string} content - The CSV content as a string. 25 + * @returns {{ header: string[]; rows: Row[] }} An object containing the header and rows. 26 + */ 13 27 export function parseCsv(content: string): { header: string[]; rows: Row[] } { 14 28 const lines = content.split(/\r?\n/).filter((l) => l.trim().length > 0); 15 29 if (lines.length === 0) return { header: [], rows: [] }; ··· 41 55 return { header, rows }; 42 56 } 43 57 58 + /** 59 + * Aggregates rows of data by a given time resolution. 60 + * @param {Row[]} rows - The array of rows to aggregate. 61 + * @param {Resolution} resolution - The time resolution to group by (e.g., "minute", "hour", "day"). 62 + * @param {Date} [start] - An optional start date to filter the rows. 63 + * @param {Date} [end] - An optional end date to filter the rows. 64 + * @returns {Array<Row & { key: string }>} An array of aggregated rows, with an added `key` property for the time bucket. 65 + */ 44 66 export function aggregateRows(rows: Row[], resolution: Resolution, start?: Date, end?: Date): Array<Row & { key: string }> { 45 67 // Group by floored time 46 68 const map = new Map<string, { t: Date; acc: Record<string, number>; cnt: Record<string, number> }>(); ··· 77 99 return out; 78 100 } 79 101 102 + /** 103 + * Infers the keys for temperature, humidity, dew point, and heat index from a CSV header. 104 + * @param {string[]} header - The array of header strings. 105 + * @returns {{ temp: string[]; hum: string[]; dew: string[]; heat: string[] }} An object containing arrays of keys for each metric. 106 + */ 80 107 export function inferAllsensorKeys(header: string[]): { temp: string[]; hum: string[]; dew: string[]; heat: string[] } { 81 108 const temp: string[] = []; 82 109 const hum: string[] = [];
+5
src/lib/db/duckdb.ts
··· 4 4 5 5 let conn: DuckDBConnection | null = null; 6 6 7 + /** 8 + * Gets a singleton DuckDB connection. 9 + * If a connection already exists, it is returned. Otherwise, a new connection is created. 10 + * @returns {Promise<DuckDBConnection>} A promise that resolves with the DuckDB connection. 11 + */ 7 12 export async function getDuckConn(): Promise<DuckDBConnection> { 8 13 if (conn) return conn; 9 14 const dataDir = path.join(process.cwd(), "data");
+55
src/lib/db/ingest.ts
··· 8 8 getMainFilesInRange, 9 9 } from "@/lib/files"; 10 10 11 + /** 12 + * Checks if a file exists at the given path. 13 + * @param {string} p - The path to the file. 14 + * @returns {Promise<boolean>} A promise that resolves to true if the file exists, false otherwise. 15 + * @private 16 + */ 11 17 async function fileExists(p: string) { 12 18 try { await fs.access(p); return true; } catch { return false; } 13 19 } 14 20 21 + /** 22 + * Gets the modification time of a file in milliseconds. 23 + * @param {string} p - The path to the file. 24 + * @returns {Promise<number>} A promise that resolves to the modification time in milliseconds. 25 + * @private 26 + */ 15 27 async function mtimeMs(p: string): Promise<number> { 16 28 const st = await fs.stat(p); 17 29 return st.mtimeMs; 18 30 } 19 31 32 + /** 33 + * Ensures that the directory for 'allsensors' Parquet files exists. 34 + * @returns {Promise<string>} A promise that resolves to the absolute path of the directory. 35 + */ 20 36 export async function ensureParquetDir(): Promise<string> { 21 37 const dir = path.join(process.cwd(), "data", "parquet", "allsensors"); 22 38 await fs.mkdir(dir, { recursive: true }); 23 39 return dir; 24 40 } 25 41 42 + /** 43 + * Ensures that the directory for 'main' Parquet files exists. 44 + * @returns {Promise<string>} A promise that resolves to the absolute path of the directory. 45 + */ 26 46 export async function ensureMainParquetDir(): Promise<string> { 27 47 const dir = path.join(process.cwd(), "data", "parquet", "main"); 28 48 await fs.mkdir(dir, { recursive: true }); 29 49 return dir; 30 50 } 31 51 52 + /** 53 + * Extracts the month (YYYYMM) from a filename. 54 + * @param {string} file - The filename. 55 + * @returns {Promise<string | null>} A promise that resolves to the month string, or null if not found. 56 + */ 32 57 export async function monthFromFilename(file: string): Promise<string | null> { 33 58 // Expect leading YYYYMM... in filename 34 59 const m = file.match(/(\d{6})/); 35 60 return m ? m[1] : null; 36 61 } 37 62 63 + /** 64 + * Selects the most likely time column name from a list of column descriptions. 65 + * @param {any[]} descRows - An array of row objects from a `DESCRIBE` query. 66 + * @returns {string | null} The name of the time column, or null if not found. 67 + * @private 68 + */ 38 69 function selectTimeColumnName(descRows: any[]): string | null { 39 70 const names = descRows.map((r: any) => String(r.column_name || r.ColumnName || r.column || "")); 40 71 const norm = (s: string) => s.toLowerCase().replace(/[^a-z]/g, ""); ··· 48 79 return null; 49 80 } 50 81 82 + /** 83 + * Ensures that a Parquet file for the 'allsensors' data of a given month exists and is up-to-date. 84 + * If the Parquet file doesn't exist or is older than the corresponding CSV file, it is created. 85 + * @param {string} month - The month in YYYYMM format. 86 + * @returns {Promise<string | null>} A promise that resolves to the path of the Parquet file, or null if the CSV file is not found. 87 + */ 51 88 export async function ensureAllsensorsParquetForMonth(month: string): Promise<string | null> { 52 89 const csvFile = await getAllsensorsFilename(month); 53 90 if (!csvFile) return null; ··· 80 117 return pqAbs; 81 118 } 82 119 120 + /** 121 + * Ensures that all 'allsensors' Parquet files for a given date range exist. 122 + * @param {Date} [start] - The start date of the range. 123 + * @param {Date} [end] - The end date of the range. 124 + * @returns {Promise<string[]>} A promise that resolves to an array of paths to the Parquet files. 125 + */ 83 126 export async function ensureAllsensorsParquetsInRange(start?: Date, end?: Date): Promise<string[]> { 84 127 const files = await getAllsensorsFilesInRange(start, end); 85 128 const months = Array.from(new Set(await Promise.all(files.map(monthFromFilename)))).filter(Boolean) as string[]; ··· 91 134 return out; 92 135 } 93 136 137 + /** 138 + * Ensures that a Parquet file for the 'main' data of a given month exists and is up-to-date. 139 + * If the Parquet file doesn't exist or is older than the corresponding CSV file, it is created. 140 + * @param {string} month - The month in YYYYMM format. 141 + * @returns {Promise<string | null>} A promise that resolves to the path of the Parquet file, or null if the CSV file is not found. 142 + */ 94 143 export async function ensureMainParquetForMonth(month: string): Promise<string | null> { 95 144 const csvFile = await getMainFilename(month); 96 145 if (!csvFile) return null; ··· 123 172 return pqAbs; 124 173 } 125 174 175 + /** 176 + * Ensures that all 'main' Parquet files for a given date range exist. 177 + * @param {Date} [start] - The start date of the range. 178 + * @param {Date} [end] - The end date of the range. 179 + * @returns {Promise<string[]>} A promise that resolves to an array of paths to the Parquet files. 180 + */ 126 181 export async function ensureMainParquetsInRange(start?: Date, end?: Date): Promise<string[]> { 127 182 const files = await getMainFilesInRange(start, end); 128 183 const months = Array.from(new Set(await Promise.all(files.map(monthFromFilename)))).filter(Boolean) as string[];
+40
src/lib/files.ts
··· 1 1 import { promises as fs } from "fs"; 2 2 import path from "path"; 3 3 4 + /** 5 + * The absolute path to the DNT directory where CSV files are stored. 6 + */ 4 7 export const DNT_DIR = path.join(process.cwd(), "DNT"); 5 8 9 + /** 10 + * Lists all CSV files in the DNT directory. 11 + * @returns {Promise<string[]>} A promise that resolves to an array of CSV filenames. 12 + */ 6 13 export async function listDntFiles(): Promise<string[]> { 7 14 const items = await fs.readdir(DNT_DIR); 8 15 return items.filter((f) => f.toLowerCase().endsWith(".csv")); 9 16 } 10 17 18 + /** 19 + * Converts a Date object to a YYYYMM number. 20 + * @param {Date} [d] - The date to convert. 21 + * @returns {number | null} The date as a YYYYMM number, or null if the date is not provided. 22 + * @private 23 + */ 11 24 function ymOf(d?: Date): number | null { 12 25 if (!d) return null; 13 26 return d.getFullYear() * 100 + (d.getMonth() + 1); 14 27 } 15 28 29 + /** 30 + * Gets all 'allsensors' CSV files within a given date range. 31 + * @param {Date} [start] - The start date of the range. 32 + * @param {Date} [end] - The end date of the range. 33 + * @returns {Promise<string[]>} A promise that resolves to an array of filenames. 34 + */ 16 35 export async function getAllsensorsFilesInRange(start?: Date, end?: Date): Promise<string[]> { 17 36 const files = await listDntFiles(); 18 37 // match case-insensitively: 202501Allsensors_A.csv / .CSV ··· 30 49 }); 31 50 } 32 51 52 + /** 53 + * Gets all 'main' CSV files within a given date range. 54 + * @param {Date} [start] - The start date of the range. 55 + * @param {Date} [end] - The end date of the range. 56 + * @returns {Promise<string[]>} A promise that resolves to an array of filenames. 57 + */ 33 58 export async function getMainFilesInRange(start?: Date, end?: Date): Promise<string[]> { 34 59 const files = await listDntFiles(); 35 60 // match case-insensitively: 202501A.csv / .CSV ··· 47 72 }); 48 73 } 49 74 75 + /** 76 + * Finds the latest file in the DNT directory that matches a regular expression. 77 + * @param {RegExp} rxLower - The regular expression to match against lowercase filenames. 78 + * @returns {Promise<string | null>} A promise that resolves to the filename, or null if no match is found. 79 + */ 50 80 export async function latestFileMatching(rxLower: RegExp): Promise<string | null> { 51 81 const files = await listDntFiles(); 52 82 const m = files.filter((f) => rxLower.test(f.toLowerCase())).sort(); 53 83 return m.length ? m[m.length - 1] : null; 54 84 } 55 85 86 + /** 87 + * Gets the 'allsensors' filename for a specific month, or the latest one if no month is provided. 88 + * @param {string} [month] - The month in YYYYMM format. 89 + * @returns {Promise<string | null>} A promise that resolves to the filename, or null if not found. 90 + */ 56 91 export async function getAllsensorsFilename(month?: string): Promise<string | null> { 57 92 const files = await listDntFiles(); 58 93 if (month && /^\d{6}$/.test(month)) { ··· 63 98 return latestFileMatching(/^\d{6}allsensors_a\.csv$/); 64 99 } 65 100 101 + /** 102 + * Gets the 'main' filename for a specific month, or the latest one if no month is provided. 103 + * @param {string} [month] - The month in YYYYMM format. 104 + * @returns {Promise<string | null>} A promise that resolves to the filename, or null if not found. 105 + */ 66 106 export async function getMainFilename(month?: string): Promise<string | null> { 67 107 const files = await listDntFiles(); 68 108 if (month && /^\d{6}$/.test(month)) {
+5
src/lib/i18n.ts
··· 1 + /** 2 + * @file This file initializes the i18next library for internationalization. 3 + * It sets up the German and English language resources and configures the default settings. 4 + * The i18n instance is initialized only once and can be safely imported in client components. 5 + */ 1 6 import i18n from "i18next"; 2 7 import { initReactI18next } from "react-i18next"; 3 8
+84
src/lib/realtimeArchiver.ts
··· 4 4 import path from "path"; 5 5 import { updateTempMinMax } from "./temp-minmax"; 6 6 7 + /** 8 + * Builds the URL parameters for the Ecowitt API request. 9 + * @param {boolean} all - Whether to fetch all data or a subset. 10 + * @returns {URLSearchParams} The URL parameters. 11 + * @private 12 + */ 7 13 function buildParams(all: boolean) { 8 14 const eco = EcoCon.getInstance().getConfig(); 9 15 const params = new URLSearchParams({ ··· 21 27 return params; 22 28 } 23 29 30 + /** 31 + * Builds the full target URL for the Ecowitt API request. 32 + * @param {boolean} all - Whether to fetch all data or a subset. 33 + * @returns {string} The full URL. 34 + */ 24 35 export function buildTargetUrl(all: boolean) { 25 36 const eco = EcoCon.getInstance().getConfig(); 26 37 const baseUrl = `https://${eco.server}/api/v3/device/real_time`; ··· 28 39 return `${baseUrl}?${qs.toString()}`; 29 40 } 30 41 42 + /** 43 + * Formats a date into a YYYYMM string. 44 + * @param {Date} d - The date to format. 45 + * @returns {string} The formatted date string. 46 + * @private 47 + */ 31 48 function yyyymm(d: Date) { 32 49 const y = d.getFullYear(); 33 50 const m = d.getMonth() + 1; ··· 35 52 return `${y}${mm}`; 36 53 } 37 54 55 + /** 56 + * Formats a date into a "YYYY/MM/DD HH:mm" string. 57 + * @param {Date} d - The date to format. 58 + * @returns {string} The formatted time string. 59 + * @private 60 + */ 38 61 function timeString(d: Date) { 39 62 // Format: 2025/08/13 12:03 (with leading zeros) 40 63 const y = d.getFullYear(); ··· 52 75 return `${y}/${mm}/${dd} ${hh}:${min}`; 53 76 } 54 77 78 + /** 79 + * Safely reads a nested property from an object using a dotted string path. 80 + * @param {any} obj - The object to read from. 81 + * @param {string} dotted - The dotted string path (e.g., "outdoor.temperature"). 82 + * @returns {any} The value of the property, or undefined if not found. 83 + * @private 84 + */ 55 85 function tryRead(obj: any, dotted: string): any { 56 86 return dotted.split(".").reduce((o, k) => (o && typeof o === "object" ? (k in o ? o[k] : undefined) : undefined), obj); 57 87 } 58 88 89 + /** 90 + * Converts a value to a number, handling various input types. 91 + * @param {any} v - The value to convert. 92 + * @returns {number | null} The numeric value, or null if conversion is not possible. 93 + * @private 94 + */ 59 95 function numVal(v: any): number | null { 60 96 if (v == null) return null; 61 97 if (typeof v === "number") return Number.isFinite(v) ? v : null; ··· 69 105 return null; 70 106 } 71 107 108 + /** 109 + * Calculates the dew point from temperature and humidity. 110 + * @param {number | null} temperature - The temperature in Celsius. 111 + * @param {number | null} humidity - The relative humidity in percent. 112 + * @returns {number | null} The dew point in Celsius, or null if inputs are invalid. 113 + * @private 114 + */ 72 115 function calculateDewPoint(temperature: number | null, humidity: number | null): number | null { 73 116 if (temperature === null || humidity === null) return null; 74 117 ··· 82 125 return Number.isFinite(dewPoint) ? Math.round(dewPoint * 10) / 10 : null; 83 126 } 84 127 128 + /** 129 + * Calculates the heat index from temperature and humidity. 130 + * @param {number | null} temperature - The temperature in Celsius. 131 + * @param {number | null} humidity - The relative humidity in percent. 132 + * @returns {number | null} The heat index in Celsius, or null if inputs are invalid. 133 + * @private 134 + */ 85 135 function calculateHeatIndex(temperature: number | null, humidity: number | null): number | null { 86 136 if (temperature === null || humidity === null) return null; 87 137 ··· 112 162 return Number.isFinite(heatIndex) ? Math.round(heatIndex * 10) / 10 : temperature; 113 163 } 114 164 165 + /** 166 + * Ensures that a directory exists, creating it if necessary. 167 + * @param {string} p - The path to the directory. 168 + * @private 169 + */ 115 170 async function ensureDir(p: string) { 116 171 await fs.mkdir(p, { recursive: true }); 117 172 } 118 173 174 + /** 175 + * Appends a row to a CSV file, creating the file and header if it doesn't exist. 176 + * @param {string} abs - The absolute path to the CSV file. 177 + * @param {string[]} header - The header row. 178 + * @param {(string | number | null)[]} row - The data row to append. 179 + * @private 180 + */ 119 181 async function appendCsv(abs: string, header: string[], row: (string | number | null)[]) { 120 182 let exists = true; 121 183 try { await fs.access(abs); } catch { exists = false; } ··· 128 190 await fs.appendFile(abs, lines.join("\n") + "\n", "utf8"); 129 191 } 130 192 193 + /** 194 + * Writes the live weather data payload to the appropriate monthly CSV files. 195 + * @param {any} payload - The data payload from the Ecowitt API. 196 + */ 131 197 export async function writeLiveToDNT(payload: any) { 132 198 const now = new Date(); 133 199 const ym = yyyymm(now); ··· 218 284 await appendCsv(mainFile, mainHeader, mainRow); 219 285 } 220 286 287 + /** 288 + * Gets the path to the realtime data cache file. 289 + * @returns {string} The cache file path. 290 + * @private 291 + */ 221 292 function cachePath() { 222 293 return path.join(process.cwd(), "DNT", "rt-last.json"); 223 294 } 224 295 296 + /** 297 + * Caches the latest realtime data record to a file. 298 + * @param {{ ok: boolean; updatedAt: string; data?: any; error?: string }} rec - The record to cache. 299 + */ 225 300 export async function setLastRealtime(rec: { ok: boolean; updatedAt: string; data?: any; error?: string }) { 226 301 const dnt = path.join(process.cwd(), "DNT"); 227 302 await ensureDir(dnt); ··· 232 307 } 233 308 } 234 309 310 + /** 311 + * Retrieves the last cached realtime data record. 312 + * @returns {Promise<{ ok: boolean; updatedAt: string; data?: any; error?: string } | null>} A promise that resolves to the cached record, or null if not found. 313 + */ 235 314 export async function getLastRealtime(): Promise<{ ok: boolean; updatedAt: string; data?: any; error?: string } | null> { 236 315 try { 237 316 const txt = await fs.readFile(cachePath(), "utf8"); ··· 241 320 } 242 321 } 243 322 323 + /** 324 + * Fetches the latest data from the Ecowitt API, archives it to CSV, and caches it. 325 + * @param {boolean} [all=true] - Whether to fetch all data or a subset. 326 + * @returns {Promise<any>} A promise that resolves to the JSON response from the API. 327 + */ 244 328 export async function fetchAndArchive(all: boolean = true) { 245 329 const target = buildTargetUrl(all); 246 330 const res = await fetch(target, { cache: "no-store" });
+39 -8
src/lib/temp-minmax.ts
··· 1 1 import fs from 'fs'; 2 2 import path from 'path'; 3 3 4 + /** 5 + * Interface for storing daily minimum and maximum temperature and humidity data. 6 + */ 4 7 interface TempMinMax { 5 - date: string; // YYYY-MM-DD format 8 + /** The date in YYYY-MM-DD format. */ 9 + date: string; 10 + /** A map of sensor keys to their min/max temperature data. */ 6 11 sensors: { 7 12 [sensorKey: string]: { 8 13 min: number; ··· 11 16 maxTime: string; // ISO timestamp 12 17 }; 13 18 }; 19 + /** A map of sensor keys to their min/max humidity data. */ 14 20 humidity: { 15 21 [sensorKey: string]: { 16 22 min: number; ··· 23 29 24 30 const DATA_FILE = path.join(process.cwd(), 'temp-minmax-data.json'); 25 31 26 - // Load existing data from file (only today's data) 32 + /** 33 + * Loads today's min/max data from the JSON file. 34 + * @returns {TempMinMax | null} The data for today, or null if it doesn't exist or an error occurs. 35 + * @private 36 + */ 27 37 function loadData(): TempMinMax | null { 28 38 try { 29 39 if (fs.existsSync(DATA_FILE)) { ··· 42 52 return null; 43 53 } 44 54 45 - // Save data to file 55 + /** 56 + * Saves the min/max data to the JSON file. 57 + * @param {TempMinMax} data - The data to save. 58 + * @private 59 + */ 46 60 function saveData(data: TempMinMax): void { 47 61 try { 48 62 fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2)); ··· 51 65 } 52 66 } 53 67 54 - // Get today's date in YYYY-MM-DD format 68 + /** 69 + * Gets today's date in YYYY-MM-DD format. 70 + * @returns {string} Today's date string. 71 + * @private 72 + */ 55 73 function getTodayDate(): string { 56 74 return new Date().toISOString().split('T')[0]; 57 75 } 58 76 59 - // Update min/max temperatures for current day 77 + /** 78 + * Updates the min/max temperature and humidity for the current day based on new sensor data. 79 + * @param {Record<string, any>} sensorData - The latest sensor data payload. 80 + */ 60 81 export function updateTempMinMax(sensorData: Record<string, any>): void { 61 82 const today = getTodayDate(); 62 83 const now = new Date().toISOString(); ··· 181 202 saveData(todayEntry!); 182 203 } 183 204 184 - // Get today's min/max temperatures 205 + /** 206 + * Gets today's min/max temperature and humidity data. 207 + * @returns {TempMinMax | null} The data for today, or null if not found. 208 + */ 185 209 export function getTodayTempMinMax(): TempMinMax | null { 186 210 return loadData(); 187 211 } 188 212 189 - // Get min/max data for a specific date (only works for today) 213 + /** 214 + * Gets the min/max data for a specific date. Note: This currently only works for today. 215 + * @param {string} date - The date in YYYY-MM-DD format. 216 + * @returns {TempMinMax | null} The data for the specified date, or null if not found. 217 + */ 190 218 export function getTempMinMaxForDate(date: string): TempMinMax | null { 191 219 const data = loadData(); 192 220 if (data && data.date === date) { ··· 195 223 return null; 196 224 } 197 225 198 - // Get all min/max data (for debugging - only today's data) 226 + /** 227 + * Gets all stored min/max data. Note: This currently only returns today's data. 228 + * @returns {TempMinMax[]} An array containing today's min/max data, or an empty array if none exists. 229 + */ 199 230 export function getAllTempMinMax(): TempMinMax[] { 200 231 const data = loadData(); 201 232 return data ? [data] : [];
+32
src/lib/time.ts
··· 1 + /** 2 + * Represents the time resolution for data aggregation. 3 + */ 1 4 export type Resolution = "minute" | "hour" | "day"; 2 5 6 + /** 7 + * Parses a timestamp string into a Date object. 8 + * Handles formats like "YYYY/M/D H:mm" or "YYYY-M-DTH:mm:ss". 9 + * @param {string} ts - The timestamp string to parse. 10 + * @returns {Date | null} A Date object, or null if parsing fails. 11 + */ 3 12 export function parseTimestamp(ts: string): Date | null { 4 13 // Expected like: 2025/8/1 0:03 (no zero padding guaranteed) 5 14 if (!ts) return null; ··· 18 27 return new Date(y, (m || 1) - 1, d || 1, hh, mm, ss, 0); 19 28 } 20 29 30 + /** 31 + * Floors a date to the specified resolution. 32 + * @param {Date} dt - The date to floor. 33 + * @param {Resolution} res - The resolution ("minute", "hour", or "day"). 34 + * @returns {Date} The floored date. 35 + */ 21 36 export function floorToResolution(dt: Date, res: Resolution): Date { 22 37 const d = new Date(dt.getTime()); 23 38 if (res === "day") { ··· 30 45 return d; 31 46 } 32 47 48 + /** 49 + * Creates a string key for a date based on the specified resolution. 50 + * @param {Date} dt - The date to create a key for. 51 + * @param {Resolution} res - The resolution. 52 + * @returns {string} The formatted key string. 53 + */ 33 54 export function keyForResolution(dt: Date, res: Resolution): string { 34 55 const y = dt.getFullYear(); 35 56 const m = dt.getMonth() + 1; ··· 41 62 return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}`; 42 63 } 43 64 65 + /** 66 + * Pads a number with a leading zero if it is less than 10. 67 + * @param {number} n - The number to pad. 68 + * @returns {string} The padded string. 69 + * @private 70 + */ 44 71 function pad(n: number): string { return n < 10 ? `0${n}` : String(n); } 45 72 73 + /** 74 + * Converts a Date object to an ISO string, adjusted for the local timezone. 75 + * @param {Date} dt - The date to convert. 76 + * @returns {string} The ISO string. 77 + */ 46 78 export function iso(dt: Date): string { 47 79 return new Date(dt.getTime() - dt.getTimezoneOffset() * 60000).toISOString(); 48 80 }
+35
src/scripts/prewarm.ts
··· 11 11 ensureMainParquetForMonth, 12 12 } from "../lib/db/ingest"; 13 13 14 + /** 15 + * Checks if a file exists at the given path. 16 + * @param p - The file path. 17 + * @returns True if the file exists, false otherwise. 18 + * @private 19 + */ 14 20 async function fileExists(p: string) { 15 21 try { await fs.access(p); return true; } catch { return false; } 16 22 } 23 + 24 + /** 25 + * Gets the modification time of a file in milliseconds. 26 + * @param p - The file path. 27 + * @returns The modification time in milliseconds since the epoch. 28 + * @private 29 + */ 17 30 async function mtimeMs(p: string): Promise<number> { 18 31 const st = await fs.stat(p); return st.mtimeMs; 19 32 } 20 33 34 + /** 35 + * Extracts the year and month (YYYYMM) from a CSV filename. 36 + * @param file - The filename. 37 + * @returns The YYYYMM string, or null if not found. 38 + * @private 39 + */ 21 40 function ymFromFilename(file: string): string | null { 22 41 const m = file.match(/(\d{6})/); 23 42 return m ? m[1] : null; 24 43 } 25 44 45 + /** 46 + * Processes a dataset ("Allsensors" or "Main") by converting new or updated CSV files 47 + * for each month into Parquet format. It checks file modification times to avoid 48 + * unnecessary conversions. 49 + * 50 + * @param label - The name of the dataset. 51 + * @param files - A list of CSV filenames for the dataset. 52 + * @param ensureDir - A function that ensures the output Parquet directory exists. 53 + * @param ensureMonth - A function that handles the conversion of a single month's CSV to Parquet. 54 + * @private 55 + */ 26 56 async function prewarmDataset( 27 57 label: "Allsensors" | "Main", 28 58 files: string[], ··· 66 96 console.log(`[prewarm] ${label}: ${built} built, ${months.length - built} up-to-date.`); 67 97 } 68 98 99 + /** 100 + * The main function for the prewarm script. 101 + * It scans for all available CSV data files and triggers the prewarming process 102 + * for both the "Allsensors" and "Main" datasets. 103 + */ 69 104 async function main() { 70 105 try { 71 106 console.log("[prewarm] Scanning DNT/ for new CSV files and materializing Parquet via DuckDB...");
+23 -2
src/types/suncalc.d.ts
··· 1 + /** 2 + * Type declarations for the 'suncalc' library, which is used for astronomical calculations. 3 + */ 1 4 declare module 'suncalc' { 5 + /** 6 + * Represents the various sun times for a given day at a specific location. 7 + */ 2 8 export type SunTimes = { 3 9 sunrise?: Date; 4 10 sunset?: Date; 5 11 [k: string]: Date | undefined; 6 12 }; 7 13 14 + /** 15 + * Represents the moon rise and set times. 16 + */ 8 17 export type MoonTimes = { 9 18 rise?: Date; 10 19 set?: Date; ··· 12 21 alwaysDown?: boolean; 13 22 }; 14 23 24 + /** 25 + * Represents the illumination and phase of the moon. 26 + */ 15 27 export type MoonIllumination = { 16 - phase: number; // 0..1 17 - fraction: number; // 0..1 28 + /** Moon phase, from 0 (new moon) to 1 (new moon). */ 29 + phase: number; 30 + /** Illuminated fraction of the moon's disk. */ 31 + fraction: number; 32 + /** Midpoint angle in radians of the illuminated limb of the moon. */ 18 33 angle: number; 19 34 }; 20 35 36 + /** 37 + * The main object provided by the suncalc library. 38 + */ 21 39 const SunCalc: { 40 + /** Calculates sun times for a given date and location. */ 22 41 getTimes(date: Date, lat: number, lon: number): SunTimes; 42 + /** Calculates moon times for a given date and location. */ 23 43 getMoonTimes(date: Date, lat: number, lon: number, inUtc?: boolean): MoonTimes; 44 + /** Calculates moon illumination data for a given date. */ 24 45 getMoonIllumination(date: Date): MoonIllumination; 25 46 }; 26 47