From 607bcd9bf50c6e529207e7713f6e8f3d43124628 Mon Sep 17 00:00:00 2001 From: Batmanisko Date: Thu, 5 Mar 2026 21:50:17 +0100 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20uprava=20refresh=20menu=20hesel=20k?= =?UTF-8?q?a=C5=BEd=C3=BD=20m=C5=AF=C5=BEe=20ud=C4=9Blat=20refresh,=20jen?= =?UTF-8?q?=20ne=20tak=20=C4=8Dasto,=20bypass=20mimo=20zdrojak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/.env.template | 6 +++++- server/src/routes/foodRoutes.ts | 15 +++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/server/.env.template b/server/.env.template index f78a16d..52e80be 100644 --- a/server/.env.template +++ b/server/.env.template @@ -43,4 +43,8 @@ # Vygenerovat pomocí: npx web-push generate-vapid-keys # VAPID_PUBLIC_KEY= # VAPID_PRIVATE_KEY= -# VAPID_SUBJECT=mailto:admin@example.com \ No newline at end of file +# VAPID_SUBJECT=mailto:admin@example.com + +# Heslo pro bypass rate limitu na endpointu /api/food/refresh (pro skripty/admin). +# Bez hesla může refresh volat každý přihlášený uživatel (podléhá rate limitu). +# REFRESH_BYPASS_PASSWORD= \ No newline at end of file diff --git a/server/src/routes/foodRoutes.ts b/server/src/routes/foodRoutes.ts index ba4bdd4..2688d7e 100644 --- a/server/src/routes/foodRoutes.ts +++ b/server/src/routes/foodRoutes.ts @@ -191,13 +191,20 @@ router.post("/updateBuyer", async (req, res, next) => { } catch (e: any) { next(e) } }); -// /api/food/refresh?type=week&heslo=docasnyheslo +// /api/food/refresh?type=week (přihlášený uživatel, nebo ?heslo=... pro bypass rate limitu) export const refreshMetoda = async (req: Request, res: Response) => { const { type, heslo } = req.query as { type?: string; heslo?: string }; - if (heslo !== "docasnyheslo" && heslo !== "tohleheslopavelnesmizjistit123") { - return res.status(403).json({ error: "Neplatné heslo" }); + const bypassPassword = process.env.REFRESH_BYPASS_PASSWORD; + const isBypass = !!bypassPassword && heslo === bypassPassword; + + if (!isBypass) { + try { + getLogin(parseToken(req)); + } catch { + return res.status(403).json({ error: "Přihlaste se prosím" }); + } } - if (!checkRateLimit("refresh") && heslo !== "tohleheslopavelnesmizjistit123") { + if (!checkRateLimit("refresh") && !isBypass) { return res.status(429).json({ error: "Refresh už se zavolal, chvíli počkej :))" }); } if (type !== "week" && type !== "day") { -- 2.47.3 From f8a65d7177b7aa4fd46654e4599414f27abd25f3 Mon Sep 17 00:00:00 2001 From: Batmanisko Date: Thu, 5 Mar 2026 22:11:45 +0100 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20detekce=20star=C3=A9ho=20menu=20Tec?= =?UTF-8?q?hTower,=20p=C5=99=C3=ADznak=20isStale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pokud TechTower vrátí menu z jiného týdne, uloží data s příznakem isStale a zobrazí varování "Data jsou z minulého týdne" místo chybové hlášky. Odstraněno staré varování o datech starších 24 hodin. --- server/src/restaurants.ts | 17 ++++++++++++++++- server/src/service.ts | 25 +++++++++++++++++-------- types/schemas/_index.yml | 3 +++ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/server/src/restaurants.ts b/server/src/restaurants.ts index 7cae72e..6680012 100644 --- a/server/src/restaurants.ts +++ b/server/src/restaurants.ts @@ -4,6 +4,10 @@ import { getMenuSladovnickaMock, getMenuTechTowerMock, getMenuUMotlikuMock, getM import { formatDate } from "./utils"; import { Food } from "../../types/gen/types.gen"; +export class StaleWeekError extends Error { + constructor(public food: Food[][]) { super('Data jsou z jiného týdne'); } +} + // Fráze v názvech jídel, které naznačují že se jedná o polévku const SOUP_NAMES = [ 'polévka', @@ -299,7 +303,6 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal } const result: Food[][] = []; - // TODO validovat, že v textu nalezeného je rozsah, do kterého spadá vstupní datum const siblings = secondTry ? $(font).parent().parent().parent().siblings('p') : $(font).parent().parent().siblings(); let parsing = false; let currentDayIndex = 0; @@ -345,6 +348,18 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal }) } } + + // Validace, zda text hlavičky obsahuje datum odpovídající požadovanému týdnu + const headerText = $(font).text().trim(); + const dateMatch = headerText.match(/(\d{1,2})\.(\d{1,2})\./); + if (dateMatch) { + const foundDay = parseInt(dateMatch[1]); + const foundMonth = parseInt(dateMatch[2]) - 1; // JS months are 0-based + if (foundDay !== firstDayOfWeek.getDate() || foundMonth !== firstDayOfWeek.getMonth()) { + throw new StaleWeekError(result); + } + } + return result; } diff --git a/server/src/service.ts b/server/src/service.ts index ff6780b..2c02c81 100644 --- a/server/src/service.ts +++ b/server/src/service.ts @@ -1,6 +1,6 @@ import { InsufficientPermissions, PizzaDayConflictError, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getIsWeekend, getWeekNumber } from "./utils"; import getStorage from "./storage"; -import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova } from "./restaurants"; +import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova, StaleWeekError } from "./restaurants"; import { getTodayMock } from "./mock"; import { removeAllUserPizzas } from "./pizza"; import { ClientData, DepartureTime, LunchChoice, PizzaDayState, Restaurant, RestaurantDayMenu, WeekMenu } from "../../types/gen/types.gen"; @@ -216,6 +216,7 @@ export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, for for (let i = 0; i < restaurantWeekFood.length && i < weekMenu.length; i++) { weekMenu[i][restaurant]!.food = restaurantWeekFood[i]; weekMenu[i][restaurant]!.lastUpdate = now; + weekMenu[i][restaurant]!.isStale = false; // Detekce uzavření pro každou restauraci switch (restaurant) { @@ -245,22 +246,34 @@ export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, for // Uložení do storage await storage.setData(getMenuKey(usedDate), weekMenu); } catch (e: any) { - console.error(`Selhalo načtení jídel pro podnik ${restaurant}`, e); + if (e instanceof StaleWeekError) { + for (let i = 0; i < e.food.length && i < weekMenu.length; i++) { + weekMenu[i][restaurant]!.food = e.food[i]; + weekMenu[i][restaurant]!.lastUpdate = now; + weekMenu[i][restaurant]!.isStale = true; + } + await storage.setData(getMenuKey(usedDate), weekMenu); + } else { + console.error(`Selhalo načtení jídel pro podnik ${restaurant}`, e); + } } } const result = weekMenu[dayOfWeekIndex][restaurant]!; - result.warnings = generateMenuWarnings(result, now); + result.warnings = generateMenuWarnings(result); return result; } /** * Generuje varování o kvalitě/úplnosti dat menu restaurace. */ -function generateMenuWarnings(menu: RestaurantDayMenu, now: number): string[] { +function generateMenuWarnings(menu: RestaurantDayMenu): string[] { const warnings: string[] = []; if (!menu.food?.length || menu.closed) { return warnings; } + if (menu.isStale) { + warnings.push('Data jsou z minulého týdne'); + } const hasSoup = menu.food.some(f => f.isSoup); if (!hasSoup) { warnings.push('Chybí polévka'); @@ -269,10 +282,6 @@ function generateMenuWarnings(menu: RestaurantDayMenu, now: number): string[] { if (missingPrice) { warnings.push('U některých jídel chybí cena'); } - const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000; - if (menu.lastUpdate && (now - menu.lastUpdate) > STALE_THRESHOLD_MS) { - warnings.push('Data jsou starší než 24 hodin'); - } return warnings; } diff --git a/types/schemas/_index.yml b/types/schemas/_index.yml index 75915d4..7992b6b 100644 --- a/types/schemas/_index.yml +++ b/types/schemas/_index.yml @@ -186,6 +186,9 @@ RestaurantDayMenu: type: array items: type: string + isStale: + description: Příznak, zda data mohou pocházet z jiného týdne + type: boolean RestaurantDayMenuMap: description: Objekt, kde klíčem je podnik ((#Restaurant)) a hodnotou denní menu daného podniku ((#RestaurantDayMenu)) type: object -- 2.47.3 From a1b1eed86da0620277438d5106d6e5fede028825 Mon Sep 17 00:00:00 2001 From: Batmanisko Date: Thu, 5 Mar 2026 22:13:19 +0100 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20p=C5=99id=C3=A1na=20strategie=20vyh?= =?UTF-8?q?led=C3=A1v=C3=A1n=C3=AD=20k=C3=B3du=20do=20CLAUDE.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 3d03f91..6f6131a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,3 +97,15 @@ cd server && yarn test # Jest (tests in server/src/tests/) - Czech naming for domain variables and UI strings; English for infrastructure code - TypeScript strict mode in both client and server - Server module resolution: Node16; Client: ESNext/bundler + +## Code Search Strategy +When searching through the project for information, use the Task tool to spawn +subagents. Each subagent should read the relevant files and return a brief +summary of what it found (not the full file contents). This keeps the main +context window small and saves tokens. Only pull in full file contents once +you've identified the specific files that matter. +When using subagents to search, each subagent should return: +- File path +- Whether it's relevant (yes/no) +- 1-3 sentence summary of what's in the file +Do NOT return full file contents in subagent responses. \ No newline at end of file -- 2.47.3