diff --git a/Dockerfile b/Dockerfile index f445128..b96db29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM node:20-alpine AS builder WORKDIR /app RUN apk add --no-cache python3 make g++ COPY package*.json ./ -RUN npm ci +RUN npm install COPY . . RUN npm run build @@ -12,7 +12,8 @@ ENV NODE_ENV=production RUN apk add --no-cache python3 make g++ 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 RUN mkdir -p data diff --git a/package-lock.json b/package-lock.json index 68a9750..01fb6e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { "name": "bodenfeuchte", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bodenfeuchte", - "version": "0.1.0", + "version": "1.0.0", "dependencies": { "better-sqlite3": "^12.10.0", "chart.js": "^4.5.0", + "chartjs-adapter-date-fns": "^3.0.0", + "date-fns": "^4.1.0", "mqtt": "^5.15.1" }, "devDependencies": { @@ -32,31 +34,6 @@ "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": { "version": "1.2.1", "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", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -1198,6 +1176,16 @@ "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": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1282,6 +1270,17 @@ "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": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/package.json b/package.json index 015561d..0a62326 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bodenfeuchte", "private": true, - "version": "1.0.0", + "version": "1.0.1", "type": "module", "scripts": { "dev": "vite dev", @@ -14,6 +14,8 @@ "dependencies": { "better-sqlite3": "^12.10.0", "chart.js": "^4.5.0", + "chartjs-adapter-date-fns": "^3.0.0", + "date-fns": "^4.1.0", "mqtt": "^5.15.1" }, "devDependencies": { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 8b73c70..a648e35 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,7 +2,9 @@ import { base } from '$app/paths'; import { onMount, onDestroy } from 'svelte'; import 'chart.js/auto'; + import 'chartjs-adapter-date-fns'; import { Chart } from 'chart.js'; + import { de } from 'date-fns/locale'; type Measurement = { timestamp: number; soil_moisture: number }; @@ -21,10 +23,10 @@ function updateChart() { if (!chart) return; - chart.data.labels = data.map(d => - new Date(d.timestamp).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) - ); - chart.data.datasets[0].data = data.map(d => d.soil_moisture); + const now = Date.now(); + chart.data.datasets[0].data = data.map(d => ({ x: d.timestamp, y: d.soil_moisture })); + chart.options.scales!['x']!.min = now - 6 * 60 * 60 * 1000; + chart.options.scales!['x']!.max = now; chart.update(); } @@ -32,14 +34,13 @@ chart = new Chart(canvas, { type: 'line', data: { - labels: [], datasets: [{ label: 'Bodenfeuchte (%)', - data: [], + data: [] as {x: number, y: number}[], borderColor: '#2980b9', backgroundColor: 'rgba(133, 183, 215, 0.2)', borderWidth: 2, - pointRadius: 0, + pointRadius: 3, pointHoverRadius: 4, fill: true, tension: 0.3, @@ -50,8 +51,35 @@ maintainAspectRatio: false, scales: { x: { - ticks: { color: '#555', maxTicksLimit: 8 }, - grid: { color: '#e5e7eb' }, + type: 'time', + 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) { + // Rebuild ticks at exactly 10-minute intervals + const min = scale.min; + const max = scale.max; + const step = 10 * 60 * 1000; + const start = Math.ceil(min / step) * step; + const ticks = []; + for (let t = start; t <= max; t += step) { + const d = new Date(t); + ticks.push({ value: t, major: d.getMinutes() === 0 }); + } + scale.ticks = ticks; + }, + ticks: { + major: { enabled: true }, + autoSkip: false, + 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: { ticks: { color: '#555', callback: v => `${v} %` },