Files
vliaudatandClaude Opus 5 063c5758a7 chore: settle the public domain and move off port 3000
The application is published at horaires.ita-ito.com. One origin serves
both the panel and the browser, so the OIDC callback, the image URL the
device is handed and the documentation all follow from this single name.

Port 3000 is already taken by the facture_ocr stack on the development
machine, so the app listens on 3010 there and the reason is written next
to the setting rather than left to be rediscovered.

The fixtures and the frozen payload snapshot move to the real domain too:
a contract snapshot carrying a hostname that never existed is a small
puzzle left for whoever reads it next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
2026-09-20 18:21:24 +02:00

130 lines
4.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Renders the panel locally, so the layout can be iterated on without a device.
*
* npm run screen:preview
*
* Writes `.preview/screen.svg`, `.preview/screen.bmp`, `.preview/screen.png`
* and an `index.html` that shows the real 1-bit image at 1:1 next to a
* magnified view, which is where clipping and thin strokes give themselves up.
*/
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import type { ScreenPayload } from '../lib/screen/contract';
import { encodeScreen } from '../lib/screen/encode';
import { SCREEN_HEIGHT, SCREEN_WIDTH } from '../lib/screen/layout';
import { renderScreenSvg } from '../lib/screen/render';
import { buildScreenPayload } from '../lib/screen/viewmodel';
import type { ScheduleContext } from '../lib/schedule/types';
const ZURICH = 'Europe/Zurich';
/** A deliberately busy week: a lunch break, a late opening and a holiday. */
const CONTEXT: ScheduleContext = {
timezone: ZURICH,
weekly: [
{ dayOfWeek: 0, isClosed: true, slots: [] },
{ dayOfWeek: 1, isClosed: true, slots: [] },
{ dayOfWeek: 2, isClosed: false, slots: [{ open: '10:00', close: '18:30' }] },
{
dayOfWeek: 3,
isClosed: false,
slots: [
{ open: '10:00', close: '13:00' },
{ open: '14:00', close: '18:30' },
],
},
{ dayOfWeek: 4, isClosed: false, slots: [{ open: '10:00', close: '18:30' }] },
{ dayOfWeek: 5, isClosed: false, slots: [{ open: '10:00', close: '18:30' }] },
{ dayOfWeek: 6, isClosed: false, slots: [{ open: '10:00', close: '18:30' }] },
],
exceptions: [
{
date: '2026-09-24',
isClosed: false,
slots: [{ open: '14:00', close: '18:30' }],
reason: 'TEMPORARY',
noteFr: 'Ouverture retardée',
noteEn: 'Late opening',
source: 'MANUAL',
},
],
vacations: [],
holidays: [
{ date: '2026-09-25', nameFr: 'Jeûne genevois', nameEn: 'Geneva Fast', isAutoClosed: true },
],
};
function payload(): ScreenPayload {
return buildScreenPayload({
now: new Date('2026-09-23T10:30:00Z'),
ctx: CONTEXT,
shopName: 'ITA ITO',
logoUrl: 'https://horaires.ita-ito.com/brand/logo-eink.png',
messages: [
{
textFr: 'Fermeture exceptionnelle jeudi matin',
textEn: 'Exceptionally closed on Thursday morning',
startsAt: null,
endsAt: null,
priority: 0,
isActive: true,
},
],
});
}
async function main(): Promise<void> {
const directory = path.join(process.cwd(), '.preview');
await mkdir(directory, { recursive: true });
const screen = payload();
const svg = await renderScreenSvg(screen);
await writeFile(path.join(directory, 'screen.svg'), svg);
await writeFile(path.join(directory, 'screen.bmp'), encodeScreen(svg, 'bmp'));
await writeFile(path.join(directory, 'screen.png'), encodeScreen(svg, 'png'));
await writeFile(path.join(directory, 'payload.json'), `${JSON.stringify(screen, null, 2)}\n`);
await writeFile(path.join(directory, 'index.html'), page());
console.log(`Preview written to ${path.relative(process.cwd(), directory)}/index.html`);
console.log('Open it in a browser, or look at screen.png directly.');
}
function page(): string {
return `<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Aperçu écran — ITA ITO</title>
<style>
body { margin: 0; padding: 32px; background: #f4f4f2; color: #141413;
font: 15px/1.5 -apple-system, system-ui, sans-serif; }
h1 { font-size: 18px; font-weight: 600; margin: 0 0 4px; }
p { margin: 0 0 24px; color: #5e5d59; }
h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .08em;
color: #5e5d59; margin: 32px 0 8px; }
img { display: block; background: #fff; border: 1px solid #d6d3c7;
image-rendering: pixelated; }
.one-to-one img { width: ${SCREEN_WIDTH}px; height: ${SCREEN_HEIGHT}px; }
.magnified img { width: ${SCREEN_WIDTH * 2}px; height: ${SCREEN_HEIGHT * 2}px; }
</style>
</head>
<body>
<h1>Aperçu de l'écran — ${SCREEN_WIDTH} × ${SCREEN_HEIGHT}, 1 bit</h1>
<p>Image réelle envoyée à l'appareil, après seuillage. Aucun gris n'est possible ici.</p>
<h2>Taille réelle</h2>
<div class="one-to-one"><img src="screen.png" alt="Aperçu à l'échelle 1:1"></div>
<h2>Agrandi ×2 — pour repérer les coupures et les traits trop fins</h2>
<div class="magnified"><img src="screen.png" alt="Aperçu agrandi"></div>
</body>
</html>
`;
}
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});