Compare commits
3 Commits
b6fdf1de98
...
a1b1eed86d
| Author | SHA1 | Date | |
|---|---|---|---|
| a1b1eed86d | |||
| f8a65d7177 | |||
| 607bcd9bf5 |
12
CLAUDE.md
12
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
|
- Czech naming for domain variables and UI strings; English for infrastructure code
|
||||||
- TypeScript strict mode in both client and server
|
- TypeScript strict mode in both client and server
|
||||||
- Server module resolution: Node16; Client: ESNext/bundler
|
- 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.
|
||||||
@@ -44,3 +44,7 @@
|
|||||||
# VAPID_PUBLIC_KEY=
|
# VAPID_PUBLIC_KEY=
|
||||||
# VAPID_PRIVATE_KEY=
|
# VAPID_PRIVATE_KEY=
|
||||||
# VAPID_SUBJECT=mailto:admin@example.com
|
# 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=
|
||||||
@@ -4,6 +4,10 @@ import { getMenuSladovnickaMock, getMenuTechTowerMock, getMenuUMotlikuMock, getM
|
|||||||
import { formatDate } from "./utils";
|
import { formatDate } from "./utils";
|
||||||
import { Food } from "../../types/gen/types.gen";
|
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
|
// Fráze v názvech jídel, které naznačují že se jedná o polévku
|
||||||
const SOUP_NAMES = [
|
const SOUP_NAMES = [
|
||||||
'polévka',
|
'polévka',
|
||||||
@@ -299,7 +303,6 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result: Food[][] = [];
|
const result: Food[][] = [];
|
||||||
// TODO validovat, že v textu nalezeného <font> je rozsah, do kterého spadá vstupní datum
|
|
||||||
const siblings = secondTry ? $(font).parent().parent().parent().siblings('p') : $(font).parent().parent().siblings();
|
const siblings = secondTry ? $(font).parent().parent().parent().siblings('p') : $(font).parent().parent().siblings();
|
||||||
let parsing = false;
|
let parsing = false;
|
||||||
let currentDayIndex = 0;
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,13 +191,20 @@ router.post("/updateBuyer", async (req, res, next) => {
|
|||||||
} catch (e: any) { next(e) }
|
} 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) => {
|
export const refreshMetoda = async (req: Request, res: Response) => {
|
||||||
const { type, heslo } = req.query as { type?: string; heslo?: string };
|
const { type, heslo } = req.query as { type?: string; heslo?: string };
|
||||||
if (heslo !== "docasnyheslo" && heslo !== "tohleheslopavelnesmizjistit123") {
|
const bypassPassword = process.env.REFRESH_BYPASS_PASSWORD;
|
||||||
return res.status(403).json({ error: "Neplatné heslo" });
|
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 :))" });
|
return res.status(429).json({ error: "Refresh už se zavolal, chvíli počkej :))" });
|
||||||
}
|
}
|
||||||
if (type !== "week" && type !== "day") {
|
if (type !== "week" && type !== "day") {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { InsufficientPermissions, PizzaDayConflictError, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getIsWeekend, getWeekNumber } from "./utils";
|
import { InsufficientPermissions, PizzaDayConflictError, formatDate, getDayOfWeekIndex, getFirstWorkDayOfWeek, getHumanDate, getIsWeekend, getWeekNumber } from "./utils";
|
||||||
import getStorage from "./storage";
|
import getStorage from "./storage";
|
||||||
import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova } from "./restaurants";
|
import { getMenuSladovnicka, getMenuTechTower, getMenuZastavkaUmichala, getMenuSenkSerikova, StaleWeekError } from "./restaurants";
|
||||||
import { getTodayMock } from "./mock";
|
import { getTodayMock } from "./mock";
|
||||||
import { removeAllUserPizzas } from "./pizza";
|
import { removeAllUserPizzas } from "./pizza";
|
||||||
import { ClientData, DepartureTime, LunchChoice, PizzaDayState, Restaurant, RestaurantDayMenu, WeekMenu } from "../../types/gen/types.gen";
|
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++) {
|
for (let i = 0; i < restaurantWeekFood.length && i < weekMenu.length; i++) {
|
||||||
weekMenu[i][restaurant]!.food = restaurantWeekFood[i];
|
weekMenu[i][restaurant]!.food = restaurantWeekFood[i];
|
||||||
weekMenu[i][restaurant]!.lastUpdate = now;
|
weekMenu[i][restaurant]!.lastUpdate = now;
|
||||||
|
weekMenu[i][restaurant]!.isStale = false;
|
||||||
|
|
||||||
// Detekce uzavření pro každou restauraci
|
// Detekce uzavření pro každou restauraci
|
||||||
switch (restaurant) {
|
switch (restaurant) {
|
||||||
@@ -245,22 +246,34 @@ export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, for
|
|||||||
// Uložení do storage
|
// Uložení do storage
|
||||||
await storage.setData(getMenuKey(usedDate), weekMenu);
|
await storage.setData(getMenuKey(usedDate), weekMenu);
|
||||||
} catch (e: any) {
|
} 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]!;
|
const result = weekMenu[dayOfWeekIndex][restaurant]!;
|
||||||
result.warnings = generateMenuWarnings(result, now);
|
result.warnings = generateMenuWarnings(result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generuje varování o kvalitě/úplnosti dat menu restaurace.
|
* Generuje varování o kvalitě/úplnosti dat menu restaurace.
|
||||||
*/
|
*/
|
||||||
function generateMenuWarnings(menu: RestaurantDayMenu, now: number): string[] {
|
function generateMenuWarnings(menu: RestaurantDayMenu): string[] {
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
if (!menu.food?.length || menu.closed) {
|
if (!menu.food?.length || menu.closed) {
|
||||||
return warnings;
|
return warnings;
|
||||||
}
|
}
|
||||||
|
if (menu.isStale) {
|
||||||
|
warnings.push('Data jsou z minulého týdne');
|
||||||
|
}
|
||||||
const hasSoup = menu.food.some(f => f.isSoup);
|
const hasSoup = menu.food.some(f => f.isSoup);
|
||||||
if (!hasSoup) {
|
if (!hasSoup) {
|
||||||
warnings.push('Chybí polévka');
|
warnings.push('Chybí polévka');
|
||||||
@@ -269,10 +282,6 @@ function generateMenuWarnings(menu: RestaurantDayMenu, now: number): string[] {
|
|||||||
if (missingPrice) {
|
if (missingPrice) {
|
||||||
warnings.push('U některých jídel chybí cena');
|
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;
|
return warnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,9 @@ RestaurantDayMenu:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
isStale:
|
||||||
|
description: Příznak, zda data mohou pocházet z jiného týdne
|
||||||
|
type: boolean
|
||||||
RestaurantDayMenuMap:
|
RestaurantDayMenuMap:
|
||||||
description: Objekt, kde klíčem je podnik ((#Restaurant)) a hodnotou denní menu daného podniku ((#RestaurantDayMenu))
|
description: Objekt, kde klíčem je podnik ((#Restaurant)) a hodnotou denní menu daného podniku ((#RestaurantDayMenu))
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
Reference in New Issue
Block a user