import { NextResponse } from 'next/server'; import { publicBaseUrl } from '@/lib/config'; import { clientIp, deviceHeader, deviceNumber } from '@/lib/device/headers'; import { computeRefreshRate } from '@/lib/device/refresh'; import { authenticateDevice } from '@/lib/device/session'; import { prisma } from '@/lib/db'; import { rateLimit } from '@/lib/ratelimit'; import { buildCurrentScreen, renderAndStore } from '@/lib/screen/service'; export const dynamic = 'force-dynamic'; /** * The only call that matters. The panel wakes, asks what to show, and goes * back to sleep for `refresh_rate` seconds. * * `filename` is the hash of the image bytes: when it matches what the firmware * already has, it skips the redraw. That is where the battery life comes from, * so the renderer must stay byte-stable for unchanged content. */ export async function GET(request: Request) { const limit = rateLimit(`display:${clientIp(request)}`, 60, 60_000); if (!limit.allowed) { return NextResponse.json( { error: 'Trop de requĂȘtes' }, { status: 429, headers: { 'Retry-After': String(limit.retryAfter) } }, ); } const device = await authenticateDevice(request); if (!device) { return NextResponse.json({ error: 'Jeton invalide' }, { status: 401 }); } const now = new Date(); const baseUrl = publicBaseUrl(request); const { payload, settings, status } = await buildCurrentScreen(now, baseUrl); const image = await renderAndStore(payload, settings.imageFormat); const refreshRate = computeRefreshRate({ now, status, timezone: settings.timezone, openSec: settings.refreshRateOpenSec, closedSec: settings.refreshRateClosedSec, }); await prisma.device.update({ where: { id: device.id }, data: { lastSeenAt: now, fwVersion: deviceHeader(request, 'fw-version'), batteryVoltage: deviceNumber(request, 'battery-voltage'), percentCharged: roundOrNull(deviceNumber(request, 'percent-charged')), rssi: roundOrNull(deviceNumber(request, 'rssi')), lastFilename: image.filename, lastRefreshRate: refreshRate, }, }); return NextResponse.json( { image_url: `${baseUrl}/api/device/image/${image.filename}`, filename: image.filename, refresh_rate: refreshRate, // Firmware updates are not this application's business: it drives a // display, it does not manage the fleet. update_firmware: false, reset_firmware: false, firmware_url: null, firmware_version: null, special_function: 'none', image_url_timeout: 0, }, { headers: { 'Cache-Control': 'no-store' } }, ); } function roundOrNull(value: number | null): number | null { return value === null ? null : Math.round(value); }