Compare commits
5 Commits
ed97694c44
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 968ff8fb71 | |||
| 6eba1be5c8 | |||
| b8b5fb3a5e | |||
| 0a3e0fc2c1 | |||
| d579cd5ac6 |
+3
-2
@@ -2,7 +2,7 @@ FROM node:20-alpine AS builder
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk add --no-cache python3 make g++
|
RUN apk add --no-cache python3 make g++
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci
|
RUN npm install
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
@@ -12,7 +12,8 @@ ENV NODE_ENV=production
|
|||||||
|
|
||||||
RUN apk add --no-cache python3 make g++
|
RUN apk add --no-cache python3 make g++
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci --omit=dev
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
RUN npm prune --omit=dev
|
||||||
COPY --from=builder /app/build ./build
|
COPY --from=builder /app/build ./build
|
||||||
|
|
||||||
RUN mkdir -p data
|
RUN mkdir -p data
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ docker buildx build \
|
|||||||
--platform linux/amd64,linux/arm64 \
|
--platform linux/amd64,linux/arm64 \
|
||||||
--build-arg BUILD_DATE="${BUILD_DATE}" \
|
--build-arg BUILD_DATE="${BUILD_DATE}" \
|
||||||
--provenance=false \
|
--provenance=false \
|
||||||
|
--progress=plain \
|
||||||
-t "${FULL_IMAGE}" \
|
-t "${FULL_IMAGE}" \
|
||||||
--push \
|
--push \
|
||||||
.
|
.
|
||||||
|
|||||||
Generated
+26
-27
@@ -1,15 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "bodenfeuchte",
|
"name": "bodenfeuchte",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bodenfeuchte",
|
"name": "bodenfeuchte",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^12.10.0",
|
"better-sqlite3": "^12.10.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"mqtt": "^5.15.1"
|
"mqtt": "^5.15.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -32,31 +34,6 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
|
||||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/wasi-threads": "1.2.1",
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/runtime": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
|
||||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
@@ -1191,6 +1168,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||||
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kurkle/color": "^0.3.0"
|
"@kurkle/color": "^0.3.0"
|
||||||
},
|
},
|
||||||
@@ -1198,6 +1176,16 @@
|
|||||||
"pnpm": ">=8"
|
"pnpm": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chartjs-adapter-date-fns": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": ">=2.8.0",
|
||||||
|
"date-fns": ">=2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
@@ -1282,6 +1270,17 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
|||||||
+3
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bodenfeuchte",
|
"name": "bodenfeuchte",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "1.1.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -14,6 +14,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^12.10.0",
|
"better-sqlite3": "^12.10.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"mqtt": "^5.15.1"
|
"mqtt": "^5.15.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+2
-2
@@ -27,8 +27,8 @@ export function insertMeasurement(soilMoisture: number): void {
|
|||||||
stmt.run(Date.now(), soilMoisture);
|
stmt.run(Date.now(), soilMoisture);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLast6Hours(): { timestamp: number; soil_moisture: number }[] {
|
export function getMeasurementsSince(hours: number): { timestamp: number; soil_moisture: number }[] {
|
||||||
const since = Date.now() - 6 * 60 * 60 * 1000;
|
const since = Date.now() - hours * 60 * 60 * 1000;
|
||||||
return getDb()
|
return getDb()
|
||||||
.prepare(
|
.prepare(
|
||||||
'SELECT timestamp, soil_moisture FROM measurements WHERE timestamp >= ? ORDER BY timestamp ASC'
|
'SELECT timestamp, soil_moisture FROM measurements WHERE timestamp >= ? ORDER BY timestamp ASC'
|
||||||
|
|||||||
+138
-12
@@ -2,10 +2,59 @@
|
|||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import 'chart.js/auto';
|
import 'chart.js/auto';
|
||||||
|
import 'chartjs-adapter-date-fns';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
|
import { de } from 'date-fns/locale';
|
||||||
|
|
||||||
type Measurement = { timestamp: number; soil_moisture: number };
|
type Measurement = { timestamp: number; soil_moisture: number };
|
||||||
|
|
||||||
|
type Range = {
|
||||||
|
label: string;
|
||||||
|
hours: number;
|
||||||
|
labelText: string;
|
||||||
|
step: number;
|
||||||
|
isMajor: (d: Date) => boolean;
|
||||||
|
formatTick: (d: Date, major: boolean) => string | string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
|
||||||
|
const RANGES: Range[] = [
|
||||||
|
{
|
||||||
|
label: '6h', hours: 6, labelText: 'Letzte 6 Stunden',
|
||||||
|
step: 10 * 60 * 1000,
|
||||||
|
isMajor: (d) => d.getMinutes() === 0,
|
||||||
|
formatTick: (d) => `${pad(d.getHours())}:${pad(d.getMinutes())}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '12h', hours: 12, labelText: 'Letzte 12 Stunden',
|
||||||
|
step: 30 * 60 * 1000,
|
||||||
|
isMajor: (d) => d.getMinutes() === 0,
|
||||||
|
formatTick: (d) => `${pad(d.getHours())}:${pad(d.getMinutes())}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1d', hours: 24, labelText: 'Letzter Tag',
|
||||||
|
step: 60 * 60 * 1000,
|
||||||
|
isMajor: (d) => d.getHours() % 6 === 0,
|
||||||
|
formatTick: (d) => `${pad(d.getHours())}:00`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '7d', hours: 7 * 24, labelText: 'Letzte 7 Tage',
|
||||||
|
step: 6 * 60 * 60 * 1000,
|
||||||
|
isMajor: (d) => d.getHours() === 0,
|
||||||
|
formatTick: (d, major) => major
|
||||||
|
? ['00:00', `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.`]
|
||||||
|
: `${pad(d.getHours())}:00`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '30d', hours: 30 * 24, labelText: 'Letzte 30 Tage',
|
||||||
|
step: 24 * 60 * 60 * 1000,
|
||||||
|
isMajor: (d) => d.getDay() === 1,
|
||||||
|
formatTick: (d) => `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let selectedRange = $state(0);
|
||||||
let data = $state<Measurement[]>([]);
|
let data = $state<Measurement[]>([]);
|
||||||
let lastUpdate = $state('');
|
let lastUpdate = $state('');
|
||||||
let canvas: HTMLCanvasElement;
|
let canvas: HTMLCanvasElement;
|
||||||
@@ -13,18 +62,34 @@
|
|||||||
let interval: ReturnType<typeof setInterval>;
|
let interval: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
const res = await fetch(`${base}/api/data`);
|
const res = await fetch(`${base}/api/data?hours=${RANGES[selectedRange].hours}`);
|
||||||
data = await res.json();
|
data = await res.json();
|
||||||
lastUpdate = new Date().toLocaleTimeString('de-DE');
|
lastUpdate = new Date().toLocaleTimeString('de-DE');
|
||||||
updateChart();
|
updateChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function selectRange(idx: number) {
|
||||||
|
selectedRange = idx;
|
||||||
|
await fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
function updateChart() {
|
function updateChart() {
|
||||||
if (!chart) return;
|
if (!chart) return;
|
||||||
chart.data.labels = data.map(d =>
|
const now = Date.now();
|
||||||
new Date(d.timestamp).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
|
const range = RANGES[selectedRange];
|
||||||
);
|
const showPoints = range.hours <= 24;
|
||||||
chart.data.datasets[0].data = data.map(d => d.soil_moisture);
|
chart.data.datasets[0].data = data.map(d => ({ x: d.timestamp, y: d.soil_moisture }));
|
||||||
|
(chart.data.datasets[0] as any).pointRadius = showPoints ? 3 : 0;
|
||||||
|
(chart.data.datasets[0] as any).pointHoverRadius = showPoints ? 4 : 0;
|
||||||
|
chart.options.scales!['x']!.min = now - range.hours * 60 * 60 * 1000;
|
||||||
|
chart.options.scales!['x']!.max = now;
|
||||||
|
if (data.length > 0) {
|
||||||
|
const values = data.map(d => d.soil_moisture);
|
||||||
|
const yMin = Math.floor(Math.min(...values) / 10) * 10;
|
||||||
|
const yMax = Math.ceil(Math.max(...values) / 10) * 10;
|
||||||
|
chart.options.scales!['y']!.min = yMin;
|
||||||
|
chart.options.scales!['y']!.max = yMin === yMax ? yMax + 10 : yMax;
|
||||||
|
}
|
||||||
chart.update();
|
chart.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,14 +97,13 @@
|
|||||||
chart = new Chart(canvas, {
|
chart = new Chart(canvas, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: [],
|
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Bodenfeuchte (%)',
|
label: 'Bodenfeuchte (%)',
|
||||||
data: [],
|
data: [] as {x: number, y: number}[],
|
||||||
borderColor: '#2980b9',
|
borderColor: '#2980b9',
|
||||||
backgroundColor: 'rgba(133, 183, 215, 0.2)',
|
backgroundColor: 'rgba(133, 183, 215, 0.2)',
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
pointRadius: 0,
|
pointRadius: 3,
|
||||||
pointHoverRadius: 4,
|
pointHoverRadius: 4,
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
@@ -50,8 +114,56 @@
|
|||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
ticks: { color: '#555', maxTicksLimit: 8 },
|
type: 'time',
|
||||||
grid: { color: '#e5e7eb' },
|
min: Date.now() - 6 * 60 * 60 * 1000,
|
||||||
|
max: Date.now(),
|
||||||
|
time: {
|
||||||
|
unit: 'minute',
|
||||||
|
displayFormats: { minute: 'HH:mm', hour: 'HH:mm' },
|
||||||
|
},
|
||||||
|
adapters: { date: { locale: de } },
|
||||||
|
afterBuildTicks(scale: any) {
|
||||||
|
const range = RANGES[selectedRange];
|
||||||
|
const min = scale.min;
|
||||||
|
const max = scale.max;
|
||||||
|
const step = range.step;
|
||||||
|
const ticks: { value: number; major: boolean }[] = [];
|
||||||
|
|
||||||
|
if (step < 60 * 60 * 1000) {
|
||||||
|
// Sub-hour: UTC alignment is fine (minutes are TZ-invariant)
|
||||||
|
const start = Math.ceil(min / step) * step;
|
||||||
|
for (let t = start; t <= max; t += step) {
|
||||||
|
ticks.push({ value: t, major: range.isMajor(new Date(t)) });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Hour+: align to local midnight so isMajor works in any timezone
|
||||||
|
const isDaily = step === 24 * 60 * 60 * 1000;
|
||||||
|
const d = new Date(min);
|
||||||
|
d.setHours(0, 0, 0, 0);
|
||||||
|
const advance = () => isDaily ? d.setDate(d.getDate() + 1) : d.setTime(d.getTime() + step);
|
||||||
|
while (d.getTime() < min) advance();
|
||||||
|
while (d.getTime() <= max) {
|
||||||
|
ticks.push({ value: d.getTime(), major: range.isMajor(d) });
|
||||||
|
advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scale.ticks = ticks;
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
major: { enabled: true },
|
||||||
|
autoSkip: false,
|
||||||
|
callback: (value: any, index: number, ticks: any[]) => {
|
||||||
|
const range = RANGES[selectedRange];
|
||||||
|
const d = new Date(value);
|
||||||
|
const major = ticks[index]?.major ?? false;
|
||||||
|
return range.formatTick(d, major);
|
||||||
|
},
|
||||||
|
color: (ctx: any) => ctx.tick?.major ? '#222' : '#aaa',
|
||||||
|
font: (ctx: any) => ctx.tick?.major ? { weight: 'bold' as const, size: 13 } : { size: 10 },
|
||||||
|
maxRotation: 0,
|
||||||
|
},
|
||||||
|
grid: { color: (ctx: any) => ctx.tick?.major ? '#999' : '#e5e7eb', lineWidth: 1 },
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
ticks: { color: '#555', callback: v => `${v} %` },
|
ticks: { color: '#555', callback: v => `${v} %` },
|
||||||
@@ -79,6 +191,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const latest = $derived(data.at(-1));
|
const latest = $derived(data.at(-1));
|
||||||
|
const currentRangeLabel = $derived(RANGES[selectedRange].labelText);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -112,9 +225,22 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Zeitbereich-Auswahl -->
|
||||||
|
<div class="flex gap-2 justify-center flex-wrap px-2">
|
||||||
|
{#each RANGES as range, i}
|
||||||
|
<button
|
||||||
|
onclick={() => selectRange(i)}
|
||||||
|
class="px-5 py-1.5 border-2 border-black rounded font-bold text-sm"
|
||||||
|
style={selectedRange === i ? 'background:#2980b9; color:white;' : 'background:rgb(231,241,247); color:black;'}
|
||||||
|
>
|
||||||
|
{range.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Diagramm -->
|
<!-- Diagramm -->
|
||||||
<div style="background:white; height:340px;" class="border-y-2 sm:border-2 border-black sm:rounded-lg p-4 pb-8">
|
<div style="background:white; height:340px;" class="border-y-2 sm:border-2 border-black sm:rounded-lg p-4 pb-8">
|
||||||
<p class="text-xs text-gray-400 mb-2">Letzte 6 Stunden · Stand: {lastUpdate || '…'}</p>
|
<p class="text-xs text-gray-400 mb-2">{currentRangeLabel} · Stand: {lastUpdate || '…'}</p>
|
||||||
{#if data.length === 0}
|
{#if data.length === 0}
|
||||||
<div class="flex items-center justify-center h-full text-gray-400 text-sm">
|
<div class="flex items-center justify-center h-full text-gray-400 text-sm">
|
||||||
Noch keine Daten vorhanden
|
Noch keine Daten vorhanden
|
||||||
@@ -128,7 +254,7 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="px-8 py-4 border-t-2 border-black flex justify-between text-xs text-gray-500">
|
<div class="px-8 py-4 border-t-2 border-black flex justify-between text-xs text-gray-500">
|
||||||
<a href="mailto:rexfue@gmail.com" class="hover:text-gray-800 transition-colors">rexfue@gmail.com</a>
|
<a href="mailto:rexfue@gmail.com" class="hover:text-gray-800 transition-colors">rexfue@gmail.com</a>
|
||||||
<span>v{__APP_VERSION__} · {__BUILD_DATE__}</span>
|
<span>V {__APP_VERSION__} - {__BUILD_DATE__}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import { getLast6Hours } from '$lib/db';
|
import { getMeasurementsSince } from '$lib/db';
|
||||||
|
|
||||||
export function GET() {
|
export function GET({ url }: { url: URL }) {
|
||||||
return json(getLast6Hours());
|
const hours = Number(url.searchParams.get('hours') ?? 6);
|
||||||
|
const safeHours = isFinite(hours) && hours > 0 ? hours : 6;
|
||||||
|
return json(getMeasurementsSince(safeHours));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user