Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| afd1ec1709 | |||
| 14e4d86719 | |||
| 8b258fd96d | |||
| 6b445af19e | |||
| d2058ff9ef | |||
| 80c4f6111c | |||
| dad182fe12 | |||
| 3811e47593 | |||
| ad6094898c | |||
| a37b3e848d |
+8
-1
@@ -11,7 +11,6 @@ coverage
|
|||||||
*.lcov
|
*.lcov
|
||||||
|
|
||||||
# logs
|
# logs
|
||||||
logs
|
|
||||||
_.log
|
_.log
|
||||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
@@ -38,3 +37,11 @@ config/local.ts
|
|||||||
|
|
||||||
# Dev mode request/response logs
|
# Dev mode request/response logs
|
||||||
debug/
|
debug/
|
||||||
|
|
||||||
|
# Runtime state
|
||||||
|
state/*
|
||||||
|
!state/.gitkeep
|
||||||
|
|
||||||
|
# Change logs
|
||||||
|
logs/*
|
||||||
|
!logs/.gitkeep
|
||||||
|
|||||||
@@ -1,15 +1,82 @@
|
|||||||
# ogaracheck
|
# ogaraCheck
|
||||||
|
|
||||||
To install dependencies:
|
Automatické sledování denního menu restaurace Ogarova pizza (Otrokovice) s notifikacemi přes Telegram.
|
||||||
|
|
||||||
|
## Co dělá
|
||||||
|
|
||||||
|
- Každých 15 minut od 8:00 do 14:00 (pracovní dny) načte dnešní menu
|
||||||
|
- V 10:00 pošle aktuální menu na Telegram (nebo oznámení "dnes menu není")
|
||||||
|
- Před 10:00 loguje změny v menu do souboru
|
||||||
|
- Po 10:00 posílá na Telegram textové změny i informace o vyprodaných položkách
|
||||||
|
|
||||||
|
## Instalace
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
To run:
|
## Konfigurace
|
||||||
|
|
||||||
```bash
|
Vytvoř soubor `config/local.ts` (je gitignored):
|
||||||
bun run index.ts
|
|
||||||
|
```ts
|
||||||
|
import type { Config } from "./default"
|
||||||
|
|
||||||
|
const localConfig: Partial<Config> = {
|
||||||
|
days: {
|
||||||
|
monday: "https://otrokovice-ogarova-pizza.cz/produkty/118_pondeli",
|
||||||
|
tuesday: "https://otrokovice-ogarova-pizza.cz/produkty/119_utery",
|
||||||
|
wednesday: "https://otrokovice-ogarova-pizza.cz/produkty/120_streda",
|
||||||
|
thursday: "https://otrokovice-ogarova-pizza.cz/produkty/121_-ctvrtek",
|
||||||
|
friday: "https://otrokovice-ogarova-pizza.cz/produkty/122_patek",
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
botToken: "váš-bot-token",
|
||||||
|
chatId: "váš-chat-id",
|
||||||
|
},
|
||||||
|
// devMode: true, // ukládá HTTP debug logy do debug/
|
||||||
|
// silent: true, // potlačí výstup na stdout
|
||||||
|
// overrideTime: "10:00", // simuluje čas pro testování
|
||||||
|
}
|
||||||
|
|
||||||
|
export default localConfig
|
||||||
```
|
```
|
||||||
|
|
||||||
This project was created using `bun init` in bun v1.3.13. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
## Manuální spuštění
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run src/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cron
|
||||||
|
|
||||||
|
Přidej do `crontab -e` — každých 15 minut od 8:00 do 13:45 v pracovní dny:
|
||||||
|
|
||||||
|
```cron
|
||||||
|
*/15 8-13 * * 1-5 cd /home/lister/workspace/ogaraCheck && /home/lister/.bun/bin/bun run src/index.ts 2>&1 | systemd-cat -t ogaraCheck
|
||||||
|
```
|
||||||
|
|
||||||
|
Sledování logů:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -t ogaraCheck -f
|
||||||
|
journalctl -t ogaraCheck --since today
|
||||||
|
```
|
||||||
|
|
||||||
|
## Struktura projektu
|
||||||
|
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
default.ts # výchozí konfigurace (commitováno)
|
||||||
|
local.ts # lokální přepisy — token, URL (gitignored)
|
||||||
|
src/
|
||||||
|
index.ts # vstupní bod, časová logika
|
||||||
|
fetcher.ts # HTTP klient s cookie jar a dev logováním
|
||||||
|
parser.ts # cheerio parser + detekce dostupnosti menu
|
||||||
|
differ.ts # porovnání stavu menu mezi spuštěními
|
||||||
|
logger.ts # zápis změn do logs/YYYY-MM-DD.log
|
||||||
|
telegram.ts # odesílání zpráv přes Telegram Bot API
|
||||||
|
logs/ # denní logy textových změn (gitignored)
|
||||||
|
state/ # stav mezi spuštěními — reference, lastKnown (gitignored)
|
||||||
|
debug/ # HTTP debug logy při devMode: true (gitignored)
|
||||||
|
```
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export interface TelegramConfig {
|
|||||||
export interface Config {
|
export interface Config {
|
||||||
days: DayUrls
|
days: DayUrls
|
||||||
devMode: boolean
|
devMode: boolean
|
||||||
|
silent: boolean
|
||||||
|
overrideTime: string | null
|
||||||
telegram: TelegramConfig
|
telegram: TelegramConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,6 +28,8 @@ const config: Config = {
|
|||||||
friday: "",
|
friday: "",
|
||||||
},
|
},
|
||||||
devMode: false,
|
devMode: false,
|
||||||
|
silent: false,
|
||||||
|
overrideTime: null,
|
||||||
telegram: {
|
telegram: {
|
||||||
botToken: "",
|
botToken: "",
|
||||||
chatId: "",
|
chatId: "",
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { ParsedMenu, MenuItem } from "./parser"
|
||||||
|
|
||||||
|
export interface TextChange {
|
||||||
|
type: "added" | "removed" | "changed"
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
field?: "name" | "description" | "price"
|
||||||
|
from?: string | number
|
||||||
|
to?: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SoldOutChange {
|
||||||
|
type: "sold-out" | "back-in-stock"
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuDiff {
|
||||||
|
textChanges: TextChange[]
|
||||||
|
soldOutChanges: SoldOutChange[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function diffMenus(prev: ParsedMenu, curr: ParsedMenu): MenuDiff {
|
||||||
|
const textChanges: TextChange[] = []
|
||||||
|
const soldOutChanges: SoldOutChange[] = []
|
||||||
|
|
||||||
|
const allItems = (menu: ParsedMenu): MenuItem[] =>
|
||||||
|
[...(menu.soup ? [menu.soup] : []), ...menu.items]
|
||||||
|
|
||||||
|
const prevMap = new Map(allItems(prev).map(i => [i.code, i]))
|
||||||
|
const currMap = new Map(allItems(curr).map(i => [i.code, i]))
|
||||||
|
|
||||||
|
for (const [code, curr] of currMap) {
|
||||||
|
const prev = prevMap.get(code)
|
||||||
|
if (!prev) {
|
||||||
|
textChanges.push({ type: "added", code, name: curr.name })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (prev.name !== curr.name) {
|
||||||
|
textChanges.push({ type: "changed", code, name: curr.name, field: "name", from: prev.name, to: curr.name })
|
||||||
|
}
|
||||||
|
if (prev.description !== curr.description) {
|
||||||
|
textChanges.push({ type: "changed", code, name: curr.name, field: "description", from: prev.description, to: curr.description })
|
||||||
|
}
|
||||||
|
if (prev.price !== curr.price) {
|
||||||
|
textChanges.push({ type: "changed", code, name: curr.name, field: "price", from: prev.price, to: curr.price })
|
||||||
|
}
|
||||||
|
if (!prev.inactive && curr.inactive) {
|
||||||
|
soldOutChanges.push({ type: "sold-out", code, name: curr.name })
|
||||||
|
} else if (prev.inactive && !curr.inactive) {
|
||||||
|
soldOutChanges.push({ type: "back-in-stock", code, name: curr.name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [code, prev] of prevMap) {
|
||||||
|
if (!currMap.has(code)) {
|
||||||
|
textChanges.push({ type: "removed", code, name: prev.name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { textChanges, soldOutChanges }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTextChange(change: TextChange): string {
|
||||||
|
switch (change.type) {
|
||||||
|
case "added":
|
||||||
|
return `PŘIDÁNO [${change.code}] ${change.name}`
|
||||||
|
case "removed":
|
||||||
|
return `ODEBRÁNO [${change.code}] ${change.name}`
|
||||||
|
case "changed":
|
||||||
|
return `ZMĚNA [${change.code}] ${change.field}: "${change.from}" → "${change.to}"`
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-3
@@ -24,9 +24,12 @@ function parseCookies(headers: Headers): Record<string, string> {
|
|||||||
const cookies: Record<string, string> = {}
|
const cookies: Record<string, string> = {}
|
||||||
const setCookieHeader = headers.getSetCookie?.() ?? []
|
const setCookieHeader = headers.getSetCookie?.() ?? []
|
||||||
for (const raw of setCookieHeader) {
|
for (const raw of setCookieHeader) {
|
||||||
const [pair] = raw.split(";")
|
const pair = raw.split(";")[0] ?? ""
|
||||||
const [name, ...rest] = pair.split("=")
|
const eqIdx = pair.indexOf("=")
|
||||||
cookies[name.trim()] = rest.join("=").trim()
|
if (eqIdx === -1) continue
|
||||||
|
const name = pair.slice(0, eqIdx).trim()
|
||||||
|
const value = pair.slice(eqIdx + 1).trim()
|
||||||
|
cookies[name] = value
|
||||||
}
|
}
|
||||||
return cookies
|
return cookies
|
||||||
}
|
}
|
||||||
|
|||||||
+118
-21
@@ -1,23 +1,82 @@
|
|||||||
import config from "../config"
|
import config from "../config"
|
||||||
import { fetchPage } from "./fetcher"
|
import { fetchPage } from "./fetcher"
|
||||||
import { parseMenu } from "./parser"
|
import { checkAvailability, parseMenu } from "./parser"
|
||||||
|
import type { ParsedMenu } from "./parser"
|
||||||
|
import { loadState, saveState } from "./state"
|
||||||
|
import { diffMenus, formatTextChange } from "./differ"
|
||||||
|
import { logChange } from "./logger"
|
||||||
import { sendMessage } from "./telegram"
|
import { sendMessage } from "./telegram"
|
||||||
|
|
||||||
type Weekday = "monday" | "tuesday" | "wednesday" | "thursday" | "friday"
|
type Weekday = "monday" | "tuesday" | "wednesday" | "thursday" | "friday"
|
||||||
|
|
||||||
const WEEKDAYS: Weekday[] = ["monday", "tuesday", "wednesday", "thursday", "friday"]
|
const WEEKDAYS: Weekday[] = ["monday", "tuesday", "wednesday", "thursday", "friday"]
|
||||||
|
|
||||||
function todayWeekday(): Weekday | null {
|
function todayWeekday(): Weekday | null {
|
||||||
const day = new Date().getDay() // 0=Sun, 1=Mon, ..., 5=Fri, 6=Sat
|
const day = new Date().getDay()
|
||||||
if (day === 0 || day === 6) return null
|
if (day === 0 || day === 6) return null
|
||||||
return WEEKDAYS[day - 1]
|
return WEEKDAYS[day - 1] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentTime(): { totalMinutes: number; dateStr: string } {
|
||||||
|
const now = new Date()
|
||||||
|
const dateStr = now.toISOString().slice(0, 10)
|
||||||
|
|
||||||
|
if (config.overrideTime) {
|
||||||
|
const parts = config.overrideTime.split(":").map(Number)
|
||||||
|
return { totalMinutes: (parts[0] ?? 0) * 60 + (parts[1] ?? 0), dateStr }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalMinutes: now.getHours() * 60 + now.getMinutes(),
|
||||||
|
dateStr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMenu(menu: ParsedMenu): string {
|
||||||
|
const lines: string[] = ["Denní menu:"]
|
||||||
|
|
||||||
|
if (menu.soup) {
|
||||||
|
lines.push(`Polévka: ${menu.soup.name} (${menu.soup.price} Kč)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = [...menu.items].sort((a, b) => {
|
||||||
|
if (a.inactive !== b.inactive) return a.inactive ? 1 : -1
|
||||||
|
const na = parseInt(a.name.match(/^(\d+)\./)?.[1] ?? "99", 10)
|
||||||
|
const nb = parseInt(b.name.match(/^(\d+)\./)?.[1] ?? "99", 10)
|
||||||
|
return na - nb
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const item of sorted) {
|
||||||
|
const sold = item.inactive ? " [VYPRODÁNO]" : ""
|
||||||
|
lines.push(`${item.name} — ${item.price} Kč${sold}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
function log(msg: string): void {
|
||||||
|
if (!config.silent) console.log(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tg(text: string): Promise<void> {
|
||||||
|
if (!config.telegram.botToken || !config.telegram.chatId) {
|
||||||
|
console.log(`TG (not configured): ${text}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await sendMessage(config.telegram.botToken, config.telegram.chatId, text)
|
||||||
|
log(`TG sent: ${text.slice(0, 80)}${text.length > 80 ? "…" : ""}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const weekday = todayWeekday()
|
const { totalMinutes, dateStr } = getCurrentTime()
|
||||||
|
|
||||||
|
if (totalMinutes >= 14 * 60) {
|
||||||
|
log("After 14:00, exiting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const weekday = todayWeekday()
|
||||||
if (!weekday) {
|
if (!weekday) {
|
||||||
console.log("Weekend — nothing to do.")
|
log("Weekend, exiting.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,28 +86,66 @@ async function main() {
|
|||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Fetching menu for ${weekday}: ${url}`)
|
const isAfter10 = totalMinutes >= 10 * 60
|
||||||
|
const state = await loadState(dateStr)
|
||||||
|
|
||||||
const { status, body } = await fetchPage(url, config.devMode)
|
const { body } = await fetchPage(url, config.devMode)
|
||||||
console.log(`Response status: ${status}`)
|
const availability = checkAvailability(body)
|
||||||
|
|
||||||
const result = parseMenu(body)
|
if (!availability.available) {
|
||||||
|
log(`Menu not available: ${availability.reason}`)
|
||||||
if (!result.valid) {
|
if (isAfter10 && !state.sentAt10) {
|
||||||
console.log("Restaurant appears closed today.")
|
await tg("Dnes menu není k dispozici.")
|
||||||
if (config.devMode) console.log("Raw preview:", result.raw)
|
state.sentAt10 = true
|
||||||
|
await saveState(state)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = JSON.stringify(result, null, 2)
|
const current = parseMenu(body)
|
||||||
console.log("Menu:", message)
|
|
||||||
|
|
||||||
if (config.telegram.botToken && config.telegram.chatId) {
|
// First run of the day
|
||||||
await sendMessage(config.telegram.botToken, config.telegram.chatId, `<pre>${message}</pre>`)
|
if (!state.reference) {
|
||||||
console.log("Sent to Telegram.")
|
state.reference = current
|
||||||
} else {
|
state.lastKnown = current
|
||||||
console.log("Telegram not configured, skipping send.")
|
if (isAfter10) {
|
||||||
|
await tg(formatMenu(current))
|
||||||
|
state.sentAt10 = true
|
||||||
}
|
}
|
||||||
|
await saveState(state)
|
||||||
|
log("First run, reference saved.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const diff = diffMenus(state.lastKnown!, current)
|
||||||
|
const hasTextChanges = diff.textChanges.length > 0
|
||||||
|
const hasSoldOut = diff.soldOutChanges.length > 0
|
||||||
|
|
||||||
|
// Log text changes (always)
|
||||||
|
for (const change of diff.textChanges) {
|
||||||
|
await logChange(dateStr, formatTextChange(change), config.silent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAfter10) {
|
||||||
|
// First send at or after 10:00
|
||||||
|
if (!state.sentAt10) {
|
||||||
|
await tg(formatMenu(current))
|
||||||
|
state.sentAt10 = true
|
||||||
|
} else if (hasTextChanges) {
|
||||||
|
await tg(formatMenu(current))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sold-out notifications (no log)
|
||||||
|
for (const change of diff.soldOutChanges) {
|
||||||
|
const msg = change.type === "sold-out"
|
||||||
|
? `Vyprodáno: ${change.name}`
|
||||||
|
: `Zpět v nabídce: ${change.name}`
|
||||||
|
await tg(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.lastKnown = current
|
||||||
|
await saveState(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(console.error)
|
main().catch(console.error)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import fs from "fs/promises"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
const LOGS_DIR = "logs"
|
||||||
|
|
||||||
|
export async function logChange(date: string, message: string, silent = false): Promise<void> {
|
||||||
|
await fs.mkdir(LOGS_DIR, { recursive: true })
|
||||||
|
const file = path.join(LOGS_DIR, `${date}.log`)
|
||||||
|
const time = new Date().toTimeString().slice(0, 8)
|
||||||
|
await fs.appendFile(file, `[${time}] ${message}\n`)
|
||||||
|
if (!silent) console.log(`LOG: ${message}`)
|
||||||
|
}
|
||||||
+65
-11
@@ -1,19 +1,73 @@
|
|||||||
import * as cheerio from "cheerio"
|
import * as cheerio from "cheerio"
|
||||||
|
|
||||||
export interface MenuResult {
|
export interface MenuItem {
|
||||||
valid: boolean
|
id: string
|
||||||
address?: string
|
code: string
|
||||||
items?: string[]
|
name: string
|
||||||
raw?: string
|
description: string
|
||||||
|
price: number
|
||||||
|
inactive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseMenu(html: string): MenuResult {
|
export interface ParsedMenu {
|
||||||
|
soup: MenuItem | null
|
||||||
|
items: MenuItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AvailabilityStatus =
|
||||||
|
| { available: false; reason: "blocked" }
|
||||||
|
| { available: false; reason: "not-today" }
|
||||||
|
| { available: true }
|
||||||
|
|
||||||
|
function parseDataAttr(raw: string): Record<string, string | null> {
|
||||||
|
const json = raw.replace(/'/g, '"')
|
||||||
|
return JSON.parse(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkAvailability(html: string): AvailabilityStatus {
|
||||||
const $ = cheerio.load(html)
|
const $ = cheerio.load(html)
|
||||||
|
|
||||||
// TODO: implement actual selectors once we see the page structure
|
const kategorieRaw = $(".js-configsource[data-kategorie]").attr("data-kategorie")
|
||||||
// For now returns raw body text so we can inspect it in dev mode
|
if (kategorieRaw) {
|
||||||
return {
|
const kategorie = parseDataAttr(kategorieRaw)
|
||||||
valid: false,
|
if (kategorie.db_zablokovana === "1") {
|
||||||
raw: $("body").text().trim().slice(0, 500),
|
return { available: false, reason: "blocked" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($("p.neaktivniKategorieInfo").length > 0) {
|
||||||
|
return { available: false, reason: "not-today" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { available: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMenu(html: string): ParsedMenu {
|
||||||
|
const $ = cheerio.load(html)
|
||||||
|
let soup: MenuItem | null = null
|
||||||
|
const items: MenuItem[] = []
|
||||||
|
|
||||||
|
$(".js-config-product[data-produkt]").each((_, el) => {
|
||||||
|
const raw = $(el).attr("data-produkt")
|
||||||
|
if (!raw) return
|
||||||
|
|
||||||
|
const data = parseDataAttr(raw)
|
||||||
|
const soldOut = $(el).find(".stitekDoporucujeme").text().trim() === "Vyprodáno"
|
||||||
|
const item: MenuItem = {
|
||||||
|
id: data.db_id ?? "",
|
||||||
|
code: data.db_kod ?? "",
|
||||||
|
name: data.db_nazev ?? "",
|
||||||
|
description: data.db_popisek ?? "",
|
||||||
|
price: parseInt(data.db_cena ?? "0", 10),
|
||||||
|
inactive: soldOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((data.db_kod ?? "").startsWith("POL")) {
|
||||||
|
soup = item
|
||||||
|
} else {
|
||||||
|
items.push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { soup, items }
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import fs from "fs/promises"
|
||||||
|
import path from "path"
|
||||||
|
import type { ParsedMenu } from "./parser"
|
||||||
|
|
||||||
|
const STATE_FILE = "state/state.json"
|
||||||
|
|
||||||
|
export interface AppState {
|
||||||
|
date: string
|
||||||
|
reference: ParsedMenu | null
|
||||||
|
lastKnown: ParsedMenu | null
|
||||||
|
sentAt10: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyState = (date: string): AppState => ({
|
||||||
|
date,
|
||||||
|
reference: null,
|
||||||
|
lastKnown: null,
|
||||||
|
sentAt10: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function loadState(today: string): Promise<AppState> {
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(STATE_FILE, "utf-8")
|
||||||
|
const state: AppState = JSON.parse(raw)
|
||||||
|
return state.date === today ? state : emptyState(today)
|
||||||
|
} catch {
|
||||||
|
return emptyState(today)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveState(state: AppState): Promise<void> {
|
||||||
|
await fs.mkdir(path.dirname(STATE_FILE), { recursive: true })
|
||||||
|
await fs.writeFile(STATE_FILE, JSON.stringify(state, null, 2))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user