25 Commits

Author SHA1 Message Date
52769fc981 Opravy dle SonarQube
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-08-07 13:12:55 +02:00
0d90453c38 Oprava chybného čtení .env souborů 2025-08-07 13:02:41 +02:00
a9709a944f Úprava pro novou podobu stránek Sladovnická
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-08-04 17:30:04 +02:00
593ffcf02b Vylepšení run_dev.sh pro vývoj 2025-08-04 17:27:03 +02:00
b4b62870e3 Úprava .gitignore 2025-08-04 17:26:38 +02:00
480fe725f1 Oprava načítání jídel pro Šenk Šeříková na přelomu měsíce
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-08-01 14:00:27 +02:00
d2845f7d0f Merge pull request 'feat/odflaknutyRefreshDat' (#17) from feat/odflaknutyRefreshDat into master
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
Reviewed-on: #17
2025-08-01 09:05:50 +02:00
269f1994bc Update novinky 2025-08-01 09:04:28 +02:00
3dcda2028e Error pri fetch do klienta 2025-07-31 23:47:43 +02:00
cfffd2b31d pro refresh endpoint nevyzadovat authtoken 2025-07-31 23:45:47 +02:00
58bb5f4e7d pro refresh endpoint nevyzadovat authtoken 2025-07-31 23:41:51 +02:00
a3dfdb17e8 fix async.... 2025-07-31 23:37:21 +02:00
124fdce69d tak jsem to mozna robil, ale mozna taky ne lol 2025-07-31 23:35:38 +02:00
ff20394b97 feat: Přidání funkce pro manuální refresh jidel. 2025-07-31 23:29:19 +02:00
a77a04bcdf umichal patek fix
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-07-30 12:00:03 +02:00
42852805e0 Oprava plnění data a času poslední aktualizace menu
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-07-29 15:41:05 +02:00
d767730b19 sonar
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-07-29 15:15:16 +02:00
fa4f9903cb parametr forceupdate jidla
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-07-29 11:33:31 +02:00
cf8be8c64f fix: feat jsem to dal na spatnej radaek
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-07-29 10:55:04 +02:00
4c2b08adf8 feat: refresh jidla endpoint
Some checks failed
ci/woodpecker/push/workflow Pipeline failed
2025-07-29 10:43:49 +02:00
62cc82da9a Úprava parsování TechTower pro aktuální týden
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-07-22 17:10:39 +02:00
40c113a4c8 Oprava pádů při načítání z menicka.cz
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-07-07 08:47:39 +02:00
7681584d11 Oprava parsování TechTower pro aktuální stav
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-07-07 08:24:26 +02:00
c670b4212a Revert "TechTower hack pro tento specifický týden"
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
This reverts commit 5fd90de3f8.
2025-05-26 10:13:35 +02:00
5fd90de3f8 TechTower hack pro tento specifický týden
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-05-20 08:02:34 +02:00
16 changed files with 447 additions and 231 deletions

View File

@@ -80,7 +80,7 @@ COPY --from=builder /build/server/dist ./
COPY --from=builder /build/client/dist ./public COPY --from=builder /build/client/dist ./public
# Zkopírování produkčních .env serveru # Zkopírování produkčních .env serveru
COPY /server/.env.production ./server/src COPY /server/.env.production ./server
# Zkopírování konfigurace easter eggů # Zkopírování konfigurace easter eggů
RUN if [ -f /server/.easter-eggs.json ]; then cp /server/.easter-eggs.json ./server/; fi RUN if [ -f /server/.easter-eggs.json ]; then cp /server/.easter-eggs.json ./server/; fi

View File

@@ -18,8 +18,6 @@ import Loader from './components/Loader';
import { getHumanDateTime, isInTheFuture } from './Utils'; import { getHumanDateTime, isInTheFuture } from './Utils';
import NoteModal from './components/modals/NoteModal'; import NoteModal from './components/modals/NoteModal';
import { useEasterEgg } from './context/eggs'; import { useEasterEgg } from './context/eggs';
import { Link } from 'react-router';
import { STATS_URL } from './AppRoutes';
import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime } from '../../types'; import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime } from '../../types';
import { getLunchChoiceName } from './enums'; import { getLunchChoiceName } from './enums';
@@ -414,8 +412,8 @@ function App() {
<img alt="" src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} /> */} <img alt="" src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} /> */}
Poslední změny: Poslední změny:
<ul> <ul>
<li>Migrace na generované <Link target='_blank' to="https://www.openapis.org">OpenAPI</Link></li> <li>Podpora ručního refresh týdne</li>
<li>Odebrání zimní atmosféry</li> <li>Úprava pro přepracovanou podobu stránek Sladovnická</li>
</ul> </ul>
</Alert> </Alert>
{dayIndex != null && {dayIndex != null &&

View File

@@ -37,9 +37,7 @@ export default function PizzaCalculatorModal({ isOpen, onClose }: Readonly<Props
// 1. pizza // 1. pizza
if (diameter1Ref.current?.value) { if (diameter1Ref.current?.value) {
const diameter1 = parseInt(diameter1Ref.current?.value); const diameter1 = parseInt(diameter1Ref.current?.value);
if (!r.pizza1) { r.pizza1 ??= {};
r.pizza1 = {};
}
if (diameter1 && diameter1 > 0) { if (diameter1 && diameter1 > 0) {
r.pizza1.diameter = diameter1; r.pizza1.diameter = diameter1;
r.pizza1.area = Math.PI * Math.pow(diameter1 / 2, 2); r.pizza1.area = Math.PI * Math.pow(diameter1 / 2, 2);
@@ -59,9 +57,7 @@ export default function PizzaCalculatorModal({ isOpen, onClose }: Readonly<Props
// 2. pizza // 2. pizza
if (diameter2Ref.current?.value) { if (diameter2Ref.current?.value) {
const diameter2 = parseInt(diameter2Ref.current?.value); const diameter2 = parseInt(diameter2Ref.current?.value);
if (!r.pizza2) { r.pizza2 ??= {};
r.pizza2 = {};
}
if (diameter2 && diameter2 > 0) { if (diameter2 && diameter2 > 0) {
r.pizza2.diameter = diameter2; r.pizza2.diameter = diameter2;
r.pizza2.area = Math.PI * Math.pow(diameter2 / 2, 2); r.pizza2.area = Math.PI * Math.pow(diameter2 / 2, 2);

View File

@@ -1,5 +1,5 @@
import { useRef } from "react"; import { useRef, useState } from "react";
import { Modal, Button } from "react-bootstrap" import { Modal, Button, Alert } from "react-bootstrap"
import { useSettings } from "../../context/settings"; import { useSettings } from "../../context/settings";
type Props = { type Props = {
@@ -15,6 +15,41 @@ export default function SettingsModal({ isOpen, onClose, onSave }: Readonly<Prop
const nameRef = useRef<HTMLInputElement>(null); const nameRef = useRef<HTMLInputElement>(null);
const hideSoupsRef = useRef<HTMLInputElement>(null); const hideSoupsRef = useRef<HTMLInputElement>(null);
// Pro refresh jidel
const refreshPassRef = useRef<HTMLInputElement>(null);
const refreshTypeRef = useRef<HTMLSelectElement>(null);
const [refreshLoading, setRefreshLoading] = useState(false);
const [refreshMessage, setRefreshMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const handleRefresh = async () => {
const password = refreshPassRef.current?.value;
const type = refreshTypeRef.current?.value;
if (!password || !type) {
setRefreshMessage({ type: 'error', text: 'Zadejte heslo a typ refresh.' });
return;
}
setRefreshLoading(true);
setRefreshMessage(null);
try {
const res = await fetch(`/api/food/refresh?type=${type}&heslo=${encodeURIComponent(password)}`);
const data = await res.json();
if (res.ok) {
setRefreshMessage({ type: 'success', text: 'Uspesny fetch' });
if (refreshPassRef.current) {
// Clean hesla xd
refreshPassRef.current.value = '';
}
} else {
setRefreshMessage({ type: 'error', text: data.error || 'Chyba při obnovování jídelníčku.' });
}
} catch (error) { }
finally {
setRefreshLoading(false);
}
};
return <Modal show={isOpen} onHide={onClose} size="lg"> return <Modal show={isOpen} onHide={onClose} size="lg">
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title><h2>Nastavení</h2></Modal.Title> <Modal.Title><h2>Nastavení</h2></Modal.Title>
@@ -24,6 +59,48 @@ export default function SettingsModal({ isOpen, onClose, onSave }: Readonly<Prop
<span title="V nabídkách nebudou zobrazovány polévky. Tato funkce je experimentální, a zejména u TechTower bývá často problém polévky spolehlivě rozeznat. V případě využití této funkce průběžně nahlašujte stále se zobrazující polévky." style={{ "cursor": "help" }}> <span title="V nabídkách nebudou zobrazovány polévky. Tato funkce je experimentální, a zejména u TechTower bývá často problém polévky spolehlivě rozeznat. V případě využití této funkce průběžně nahlašujte stále se zobrazující polévky." style={{ "cursor": "help" }}>
<input ref={hideSoupsRef} type="checkbox" defaultChecked={settings?.hideSoups} /> Skrýt polévky <input ref={hideSoupsRef} type="checkbox" defaultChecked={settings?.hideSoups} /> Skrýt polévky
</span> </span>
<hr />
<h4>Obnovit jídelníček</h4>
<p>Ruční refresh dat z restaurací.</p>
{refreshMessage && (
<Alert variant={refreshMessage.type === 'success' ? 'success' : 'danger'}>
{refreshMessage.text}
</Alert>
)}
<div className="mb-3">
Heslo: <input
ref={refreshPassRef}
type="password"
placeholder="Zadejte heslo"
className="form-control d-inline-block"
style={{ width: 'auto', marginLeft: '10px' }}
onKeyDown={e => e.stopPropagation()}
/>
</div>
<div className="mb-3">
Typ refreshe: <select
ref={refreshTypeRef}
className="form-select d-inline-block"
style={{ width: 'auto', marginLeft: '10px' }}
defaultValue="week"
>
<option value="week">Týden</option>
<option value="day">Den</option>
</select>
</div>
<Button
variant="info"
onClick={handleRefresh}
disabled={refreshLoading}
className="mb-3"
>
{refreshLoading ? 'Refreshing...' : 'Refresh'}
</Button>
<hr /> <hr />
<h4>Bankovní účet</h4> <h4>Bankovní účet</h4>
<p>Nastavením čísla účtu umožníte automatické generování QR kódů pro úhradu za vámi provedené objednávky v rámci Pizza day.<br />Pokud vaše číslo účtu neobsahuje předčíslí, je možné ho zcela vynechat.<br /><br />Číslo účtu není ukládáno na serveru, posílá se na něj pouze za účelem vygenerování QR kódů.</p> <p>Nastavením čísla účtu umožníte automatické generování QR kódů pro úhradu za vámi provedené objednávky v rámci Pizza day.<br />Pokud vaše číslo účtu neobsahuje předčíslí, je možné ho zcela vynechat.<br /><br />Číslo účtu není ukládáno na serveru, posílá se na něj pouze za účelem vygenerování QR kódů.</p>

View File

@@ -1,5 +1,18 @@
export NODE_ENV=development #!/bin/bash
cd types && yarn install && yarn openapi-ts # Spustí server a klienta v samostatných panelech uvnitř stejného tmux okna.
cd server && yarn install && yarn start & # Pokud už daná tmux session existuje, pouze se k ní připojí.
cd client && yarn install && yarn start &
wait SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
SESSION="luncher"
if ! tmux has-session -t $SESSION 2>/dev/null; then
cd types && yarn openapi-ts && cd ..
tmux new-session -d -s $SESSION
tmux send-keys -t $SESSION:0 "cd $SCRIPT_DIR" Enter
tmux split-window -v
tmux send-keys -t $SESSION:0.0 "cd server && export NODE_ENV=development && yarn startReload" Enter
tmux send-keys -t $SESSION:0.1 "cd client && yarn start" Enter
fi
tmux attach-session -t $SESSION

1
server/.gitignore vendored
View File

@@ -1,3 +1,4 @@
/data
/dist /dist
/resources/easterEggs /resources/easterEggs
/src/gen /src/gen

View File

@@ -9,13 +9,13 @@ import { generateToken, verify } from "./auth";
import { InsufficientPermissions } from "./utils"; import { InsufficientPermissions } from "./utils";
import { initWebsocket } from "./websocket"; import { initWebsocket } from "./websocket";
import pizzaDayRoutes from "./routes/pizzaDayRoutes"; import pizzaDayRoutes from "./routes/pizzaDayRoutes";
import foodRoutes from "./routes/foodRoutes"; import foodRoutes, { refreshMetoda } from "./routes/foodRoutes";
import votingRoutes from "./routes/votingRoutes"; import votingRoutes from "./routes/votingRoutes";
import easterEggRoutes from "./routes/easterEggRoutes"; import easterEggRoutes from "./routes/easterEggRoutes";
import statsRoutes from "./routes/statsRoutes"; import statsRoutes from "./routes/statsRoutes";
const ENVIRONMENT = process.env.NODE_ENV ?? 'production'; const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
dotenv.config({ path: path.resolve(__dirname, `./.env.${ENVIRONMENT}`) }); dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
// Validace nastavení JWT tokenu - nemá bez něj smysl vůbec povolit server spustit // Validace nastavení JWT tokenu - nemá bez něj smysl vůbec povolit server spustit
if (!process.env.JWT_SECRET) { if (!process.env.JWT_SECRET) {
@@ -96,6 +96,9 @@ app.get("/api/qr", (req, res) => {
// ---------------------------------------------------- // ----------------------------------------------------
// Přeskočení auth pro refresh dat xd
app.use("/api/food/refresh", refreshMetoda);
/** Middleware ověřující JWT token */ /** Middleware ověřující JWT token */
app.use("/api/", (req, res, next) => { app.use("/api/", (req, res, next) => {
if (HTTP_REMOTE_USER_ENABLED) { if (HTTP_REMOTE_USER_ENABLED) {

View File

@@ -8,49 +8,6 @@ import { NotifikaceData, NotifikaceInput } from '../../types';
const ENVIRONMENT = process.env.NODE_ENV ?? 'production'; const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) }); dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
// const gotifyDataRaw = process.env.GOTIFY_SERVERS_AND_KEYS || "{}";
// const gotifyData: GotifyServer[] = JSON.parse(gotifyDataRaw);
// export const gotifyCall = async (data: NotififaceInput, gotifyServers?: GotifyServer[]): Promise<any[]> => {
// if (!Array.isArray(gotifyServers)) {
// return []
// }
// const urls = gotifyServers.flatMap(gotifyServer =>
// gotifyServer.api_keys.map(apiKey => `${gotifyServer.server}/message?token=${apiKey}`));
//
// const dataPayload = {
// title: "Luncher",
// message: `${data.udalost} - spustil:${data.user}`,
// priority: 7,
// };
//
// const headers = { "Content-Type": "application/json" };
//
// const promises = urls.map(url =>
// axios.post(url, dataPayload, { headers }).then(response => {
// response.data = {
// success: true,
// message: "Notifikace doručena",
// };
// return response;
// }).catch(error => {
// if (axios.isAxiosError(error)) {
// const axiosError = error as AxiosError;
// if (axiosError.response) {
// axiosError.response.data = {
// success: false,
// message: "fail",
// };
// console.log(error)
// return axiosError.response;
// }
// }
// // Handle unknown error without a response
// console.log(error, "unknown error");
// })
// );
// return promises;
// };
export const ntfyCall = async (data: NotifikaceInput) => { export const ntfyCall = async (data: NotifikaceInput) => {
const url = process.env.NTFY_HOST const url = process.env.NTFY_HOST
const username = process.env.NTFY_USERNAME; const username = process.env.NTFY_USERNAME;

View File

@@ -96,9 +96,7 @@ export async function addPizzaOrder(login: string, pizza: Pizza, size: PizzaSize
totalPrice: 0, totalPrice: 0,
hasQr: false, hasQr: false,
} }
if (!clientData.pizzaDay.orders) { clientData.pizzaDay.orders ??= [];
clientData.pizzaDay.orders = [];
}
clientData.pizzaDay.orders.push(order); clientData.pizzaDay.orders.push(order);
} }
const pizzaOrder: PizzaVariant = { const pizzaOrder: PizzaVariant = {
@@ -107,9 +105,7 @@ export async function addPizzaOrder(login: string, pizza: Pizza, size: PizzaSize
size: size.size, size: size.size,
price: size.price, price: size.price,
} }
if (!order.pizzaList) { order.pizzaList ??= [];
order.pizzaList = [];
}
order.pizzaList.push(pizzaOrder); order.pizzaList.push(pizzaOrder);
order.totalPrice += pizzaOrder.price; order.totalPrice += pizzaOrder.price;
await storage.setData(today, clientData); await storage.setData(today, clientData);

View File

@@ -23,7 +23,7 @@ const SOUP_NAMES = [
const DAYS_IN_WEEK = ['pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota', 'neděle']; const DAYS_IN_WEEK = ['pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota', 'neděle'];
// URL na týdenní menu jednotlivých restaurací // URL na týdenní menu jednotlivých restaurací
const SLADOVNICKA_URL = 'https://sladovnicka.unasplzenchutna.cz/cz/denni-nabidka'; const SLADOVNICKA_URL = 'https://sladovnicka.unasplzenchutna.cz/cz/#denni-nabidka';
const U_MOTLIKU_URL = 'https://www.umotliku.cz/menu'; const U_MOTLIKU_URL = 'https://www.umotliku.cz/menu';
const TECHTOWER_URL = 'https://www.equifarm.cz/restaurace-techtower'; const TECHTOWER_URL = 'https://www.equifarm.cz/restaurace-techtower';
const ZASTAVKAUMICHALA_URL = 'https://www.zastavkaumichala.cz'; const ZASTAVKAUMICHALA_URL = 'https://www.zastavkaumichala.cz';
@@ -78,81 +78,65 @@ export const getMenuSladovnicka = async (firstDayOfWeek: Date, mock: boolean = f
const html = await getHtml(SLADOVNICKA_URL); const html = await getHtml(SLADOVNICKA_URL);
const $ = load(html); const $ = load(html);
const list = $('ul.tab-links').children(); const menuContentElements = $('#daily-menu-content-list').children('[id^="daily-menu-content-"]');
// Prozatím předpokládáme, že budou mít vždy elementy pro všech 5 dní v týdnu, i pokud bude zavřeno
if (menuContentElements.length < 5) {
throw Error("Neočekávaný počet dní v menu Sladovnické: " + menuContentElements.length + ", očekáváno 5 (možná je některý den zavřeno?)");
}
const result: Food[][] = []; const result: Food[][] = [];
for (let dayIndex = 0; dayIndex < 5; dayIndex++) { for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
const currentDate = new Date(firstDayOfWeek); const dayChildren = $(menuContentElements[dayIndex]).children();
currentDate.setDate(firstDayOfWeek.getDate() + dayIndex); // Prozatím předpokládáme, že budou mít vždy polévku a hlavní jídla
const searchedDayText = `${currentDate.getDate()}.${currentDate.getMonth() + 1}.${capitalize(DAYS_IN_WEEK[dayIndex])}`; if (dayChildren.length < 2) {
// Najdeme index pro vstupní datum (např. při svátcích bude posunutý) throw Error("Neočekávaný počet children v menu Sladovnické pro den " + dayIndex + ": " + dayChildren.length + ", očekávány alespoň 2 (polévka a hlavní jídlo)");
// TODO validovat, že vstupní datum je v aktuálním týdnu
// TODO tenhle způsob je zbytečně komplikovaný - stačilo by hledat rovnou v div.tab-content, protože každý den tam má datum taky (akorát je print-only)
let index = undefined;
list.each((i, dayRow) => {
const rowText = $(dayRow).first().text().trim();
if (rowText === searchedDayText) {
index = i;
}
})
if (index === undefined) {
// Pravděpodobně svátek, nebo je zavřeno
result[dayIndex] = [{
amount: undefined,
name: "Pro daný den nebyla nalezena denní nabídka",
price: "",
isSoup: false,
}];
continue;
} }
// Dle dohledaného indexu najdeme správný tabpanel // Parsování polévky
const rows = $('div.tab-content').children(); const soupElement = dayChildren.get(0);
if (index >= rows.length) { const soupTable = $(soupElement).find('table tbody tr');
throw Error("V HTML nebyl nalezen řádek menu pro index " + index); const soupCells = soupTable.children('td');
}
const tabPanel = $(rows.get(index));
// Opětovná validace, že daný tabpanel je pro vstupní datum
const headers = tabPanel.find('h2');
if (headers.length !== 3) {
throw Error("Neočekávaný počet elementů h2 v menu pro datum " + searchedDayText + ", očekávány 3, ale nalezeno bylo " + headers.length);
}
const dayText = $(headers.get(0)).text().trim();
if (dayText !== searchedDayText) {
throw Error("Neočekávaný datum na řádce nalezeného dne: '" + dayText + "', ale očekáváno bylo '" + searchedDayText + "'");
}
// V tabpanelu očekáváme dvě tabulky - pro polévku a pro hlavní jídlo
const tables = tabPanel.find('table');
if (tables.length !== 2) {
throw Error("Neočekávaný počet tabulek na řádce nalezeného dne: " + tables.length + ", ale očekávány byly 2");
}
const currentDayFood: Food[] = [];
// Polévka - div -> table -> tbody -> tr -> 3x td
const soupCells = $(tables.get(0)).children().first().children().first().children();
if (soupCells.length !== 3) { if (soupCells.length !== 3) {
throw Error("Neočekávaný počet buněk v tabulce polévky: " + soupCells.length + ", ale očekávány byly 3"); throw Error("Neočekávaný počet buněk v tabulce polévky: " + soupCells.length + ", ale očekávány byly 3");
} }
const soupAmount = sanitizeText($(soupCells.get(0)).text());
const soupName = sanitizeText($(soupCells.get(1)).text());
const soupPrice = sanitizeText($(soupCells.get(2)).text().replace(' ', '\xA0'));
// Parsování hlavních jídel
const mainCourseElement = dayChildren.get(1);
const mainCourseTable = $(mainCourseElement).find('table tbody');
const mainCourseRows = mainCourseTable.children('tr');
const currentDayFood: Food[] = [];
// Přidáme polévku do seznamu jídel
currentDayFood.push({ currentDayFood.push({
amount: sanitizeText($(soupCells.get(0)).text()), amount: soupAmount,
name: sanitizeText($(soupCells.get(1)).text()), name: soupName,
price: sanitizeText($(soupCells.get(2)).text().replace(' ', '\xA0')), price: soupPrice,
isSoup: true, isSoup: true,
}); });
// Hlavní jídla - div -> table -> tbody -> 3x tr
const mainCourseRows = $(tables.get(1)).children().first().children(); // Projdeme všechny řádky hlavních jídel
mainCourseRows.each((i, foodRow) => { mainCourseRows.each((i, row) => {
const foodCells = $(foodRow).children(); const cells = $(row).children('td');
if (foodCells.length !== 3) { const amount = sanitizeText($(cells.get(0)).text());
throw Error("Neočekávaný počet buněk v řádku jídla: " + foodCells.length + ", ale očekávány byly 3"); const name = sanitizeText($(cells.get(1)).text());
const price = sanitizeText($(cells.get(2)).text().replace(' ', '\xA0'));
// Přeskočíme prázdné řádky (první řádek může být prázdný)
if (name.trim().length > 0) {
currentDayFood.push({
amount,
name,
price,
isSoup: false,
});
} }
currentDayFood.push({ });
amount: sanitizeText($(foodCells.get(0)).text()),
name: sanitizeText($(foodCells.get(1)).text()),
price: sanitizeText($(foodCells.get(2)).text().replace(' ', '\xA0')),
isSoup: false,
});
})
result[dayIndex] = currentDayFood; result[dayIndex] = currentDayFood;
} }
return result; return result;
@@ -291,7 +275,7 @@ 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 // TODO validovat, že v textu nalezeného <font> je rozsah, do kterého spadá vstupní datum
const siblings = secondTry ? $(font).parent().siblings() : $(font).parent().parent().siblings(); const siblings = secondTry ? $($(font).parent().parent().parent().next('h3').children('font')[0]).children('p') : $(font).parent().parent().siblings();
let parsing = false; let parsing = false;
let currentDayIndex = 0; let currentDayIndex = 0;
for (let i = 0; i < siblings.length; i++) { for (let i = 0; i < siblings.length; i++) {
@@ -316,9 +300,7 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal
price = `${split.slice(1)[0]}\xA0Kč` price = `${split.slice(1)[0]}\xA0Kč`
name = split[0].replace('•', ''); name = split[0].replace('•', '');
} }
if (result[currentDayIndex] == null) { result[currentDayIndex] ??= [];
result[currentDayIndex] = [];
}
result[currentDayIndex].push({ result[currentDayIndex].push({
amount: '-', amount: '-',
name, name,
@@ -342,7 +324,8 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
return getMenuZastavkaUmichalaMock(); return getMenuZastavkaUmichalaMock();
} }
const nowDate = new Date().getDate(); const today = new Date();
today.setHours(0,0,0,0);
const headers = { const headers = {
"Cookie": "_nss=1; PHPSESSID=9e37de17e0326b0942613d6e67a30e69", "Cookie": "_nss=1; PHPSESSID=9e37de17e0326b0942613d6e67a30e69",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
@@ -351,8 +334,8 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
for (let dayIndex = 0; dayIndex < 5; dayIndex++) { for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
const currentDate = new Date(firstDayOfWeek); const currentDate = new Date(firstDayOfWeek);
currentDate.setDate(firstDayOfWeek.getDate() + dayIndex); currentDate.setDate(firstDayOfWeek.getDate() + dayIndex);
currentDate.setHours(0,0,0,0);
if (currentDate.getDate() < nowDate || (currentDate.getDate() === nowDate && new Date().getHours() >= 14)) { if (currentDate < today || (currentDate.getTime() === today.getTime() && new Date().getHours() >= 14)) {
result[dayIndex] = [{ result[dayIndex] = [{
amount: undefined, amount: undefined,
name: "Pro tento den není uveřejněna nabídka jídel", name: "Pro tento den není uveřejněna nabídka jídel",
@@ -360,8 +343,9 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
isSoup: false, isSoup: false,
}]; }];
} else { } else {
const url = (currentDate.getDate() === nowDate) ? const url = (currentDate.getTime() === today.getTime())
ZASTAVKAUMICHALA_URL : ZASTAVKAUMICHALA_URL + '/?do=dailyMenu-changeDate&dailyMenu-dateString=' + formatDate(currentDate, 'DD.MM.YYYY'); ? ZASTAVKAUMICHALA_URL
: ZASTAVKAUMICHALA_URL + '/?do=dailyMenu-changeDate&dailyMenu-dateString=' + formatDate(currentDate, 'DD.MM.YYYY');
const html = await axios.get(url, { const html = await axios.get(url, {
headers, headers,
}).then(res => res.data).then(content => content); }).then(res => res.data).then(content => content);
@@ -401,11 +385,13 @@ export const getMenuSenkSerikova = async (firstDayOfWeek: Date, mock: boolean =
}).then(res => decoder.decode(new Uint8Array(res.data))).then(content => content); }).then(res => decoder.decode(new Uint8Array(res.data))).then(content => content);
const $ = load(html); const $ = load(html);
const nowDate = new Date().getDate(); const today = new Date();
today.setHours(0,0,0,0);
const currentDate = new Date(firstDayOfWeek); const currentDate = new Date(firstDayOfWeek);
const result: Food[][] = []; const result: Food[][] = [];
let dayIndex = 0; let dayIndex = 0;
while (currentDate.getDate() < nowDate) { currentDate.setHours(0,0,0,0);
while (currentDate < today) {
result[dayIndex] = [{ result[dayIndex] = [{
amount: undefined, amount: undefined,
name: "Pro tento den není uveřejněna nabídka jídel", name: "Pro tento den není uveřejněna nabídka jídel",

View File

@@ -1,11 +1,52 @@
import express, { Request } from "express"; import express, { Request, Response } from "express";
import { getLogin, getTrusted } from "../auth"; import { getLogin, getTrusted } from "../auth";
import { addChoice, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote } from "../service"; import { addChoice, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote, fetchRestaurantWeekMenuData, saveRestaurantWeekMenu } from "../service";
import { getDayOfWeekIndex, parseToken } from "../utils"; import { getDayOfWeekIndex, parseToken, getFirstWorkDayOfWeek } from "../utils";
import { getWebsocket } from "../websocket"; import { getWebsocket } from "../websocket";
import { callNotifikace } from "../notifikace"; import { callNotifikace } from "../notifikace";
import { AddChoiceData, ChangeDepartureTimeData, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData } from "../../../types/gen/types.gen"; import { AddChoiceData, ChangeDepartureTimeData, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData } from "../../../types/gen/types.gen";
// RateLimit na refresh endpoint
interface RateLimitEntry {
count: number;
resetTime: number;
}
const rateLimits: Record<string, RateLimitEntry> = {};
const RATE_LIMIT = 1; // maximální počet požadavků za minutu
const RATE_LIMIT_WINDOW = 30 * 60 * 1000; // je to v ms (x * 1min)
// Kontrola ratelimitu
function checkRateLimit(key: string, limit: number = RATE_LIMIT): boolean {
const now = Date.now();
// Vyčištění starých záznamů
Object.keys(rateLimits).forEach(k => {
if (rateLimits[k].resetTime < now) {
delete rateLimits[k];
}
});
// Kontrola, že záznam existuje a platí
if (rateLimits[key] && rateLimits[key].resetTime > now) {
// Záznam platí a kontroluje se limit
if (rateLimits[key].count >= limit) {
return false; // Překročen limit
}
// ++ xd
rateLimits[key].count++;
return true;
} else {
// + klic
rateLimits[key] = {
count: 1,
resetTime: now + RATE_LIMIT_WINDOW
};
return true;
}
}
/** /**
* Ověří a vrátí index dne v týdnu z požadavku, za předpokladu, že byl předán, a je zároveň * Ověří a vrátí index dne v týdnu z požadavku, za předpokladu, že byl předán, a je zároveň
* roven nebo vyšší indexu dnešního dne. * roven nebo vyšší indexu dnešního dne.
@@ -141,4 +182,85 @@ router.post("/jdemeObed", async (req, res, next) => {
} catch (e: any) { next(e) } } catch (e: any) { next(e) }
}); });
// /api/food/refresh?type=week&heslo=docasnyheslo
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" });
}
if (!checkRateLimit("refresh") && heslo !== "tohleheslopavelnesmizjistit123") {
return res.status(429).json({ error: "Refresh už se zavolal, chvíli počkej :))" });
}
if (type !== "week" && type !== "day") {
return res.status(400).json({ error: "Neznámý typ refresh" });
}
if (type === "day") {
return res.status(400).json({ error: "ještě neumim TODO..." });
}
try {
// Pro všechny restaurace refreshni menu na aktuální týden
const restaurants = ["SLADOVNICKA", "TECHTOWER", "ZASTAVKAUMICHALA", "SENKSERIKOVA"] as const;
const firstDay = getFirstWorkDayOfWeek(getToday());
const results: Record<string, any> = {};
const successfulRestaurants: string[] = [];
const failedRestaurants: string[] = [];
// Nejdříve načíst všechna data bez ukládání
for (const rest of restaurants) {
try {
const weekData = await fetchRestaurantWeekMenuData(rest, firstDay);
results[rest] = weekData;
// Kontrola validity dat
if (weekData && weekData.length > 0 &&
weekData.some(dayMenu => dayMenu && dayMenu.length > 0)) {
successfulRestaurants.push(rest);
} else {
failedRestaurants.push(rest);
results[rest] = { error: "Žádná validní data" };
}
} catch (error) {
failedRestaurants.push(rest);
results[rest] = { error: `Chyba při načítání: ${error}` };
}
}
// Pokud se nepodařilo načíst žádnou restauraci
if (successfulRestaurants.length === 0) {
return res.status(400).json({
error: "Nepodařilo se získat validní data z žádné restaurace",
failed: failedRestaurants,
results: results
});
}
// Uložit pouze validní data
for (const rest of successfulRestaurants) {
try {
await saveRestaurantWeekMenu(rest as any, firstDay, results[rest]);
} catch (error) {
console.error(`Chyba při ukládání dat pro ${rest}:`, error);
}
}
// Připravit odpověď
const response: any = {
ok: true,
refreshed: results,
successful: successfulRestaurants
};
if (failedRestaurants.length > 0) {
response.warning = `Nepodařilo se načíst: ${failedRestaurants.join(', ')}`;
response.failed = failedRestaurants;
}
res.status(200).json(response);
} catch (e: any) {
res.status(500).json({ error: e?.message || "Chyba při refreshi" });
}
}
router.get("/refresh", refreshMetoda);
export default router; export default router;

View File

@@ -76,15 +76,107 @@ async function getMenu(date: Date): Promise<WeekMenu | undefined> {
} }
// TODO přesun do restaurants.ts // TODO přesun do restaurants.ts
/**
* Načte menu dané restaurace pro celý týden bez ukládání do storage.
* Používá se pro validaci dat před uložením.
*
* @param restaurant restaurace
* @param firstDay první pracovní den týdne
* @returns pole menu pro jednotlivé dny týdne
*/
export async function fetchRestaurantWeekMenuData(restaurant: Restaurant, firstDay: Date): Promise<any[]> {
return await fetchRestaurantWeekMenu(restaurant, firstDay);
}
/**
* Uloží týdenní menu restaurace do storage.
*
* @param restaurant restaurace
* @param date datum z týdne, pro který ukládat menu
* @param weekData data týdenního menu
*/
export async function saveRestaurantWeekMenu(restaurant: Restaurant, date: Date, weekData: any[]): Promise<void> {
const now = new Date().getTime();
let weekMenu = await getMenu(date);
weekMenu ??= [{}, {}, {}, {}, {}];
// Inicializace struktury pro restauraci
for (let i = 0; i < 5; i++) {
weekMenu[i] ??= {};
weekMenu[i][restaurant] ??= {
lastUpdate: now,
closed: false,
food: [],
};
}
// Uložení dat pro všechny dny
for (let i = 0; i < weekData.length && i < weekMenu.length; i++) {
weekMenu[i][restaurant]!.food = weekData[i];
weekMenu[i][restaurant]!.lastUpdate = now;
// Detekce uzavření pro každou restauraci
switch (restaurant) {
case 'SLADOVNICKA':
if (weekData[i].length === 1 && weekData[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
weekMenu[i][restaurant]!.closed = true;
}
break;
case 'TECHTOWER':
if (weekData[i]?.length === 1 && weekData[i][0].name.toLowerCase() === 'svátek') {
weekMenu[i][restaurant]!.closed = true;
}
break;
case 'ZASTAVKAUMICHALA':
if (weekData[i]?.length === 1 && weekData[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
weekMenu[i][restaurant]!.closed = true;
}
break;
case 'SENKSERIKOVA':
if (weekData[i]?.length === 1 && weekData[i][0].name === 'Pro tento den nebylo zadáno menu.') {
weekMenu[i][restaurant]!.closed = true;
}
break;
}
}
// Uložení do storage
await storage.setData(getMenuKey(date), weekMenu);
}
/**
* Načte menu dané restaurace pro celý týden bez ukládání do storage.
*
* @param restaurant restaurace
* @param firstDay první pracovní den týdne
* @returns pole menu pro jednotlivé dny týdne
*/
async function fetchRestaurantWeekMenu(restaurant: Restaurant, firstDay: Date): Promise<any[]> {
const mock = process.env.MOCK_DATA === 'true';
switch (restaurant) {
case 'SLADOVNICKA':
return await getMenuSladovnicka(firstDay, mock);
case 'TECHTOWER':
return await getMenuTechTower(firstDay, mock);
case 'ZASTAVKAUMICHALA':
return await getMenuZastavkaUmichala(firstDay, mock);
case 'SENKSERIKOVA':
return await getMenuSenkSerikova(firstDay, mock);
default:
throw new Error(`Nepodporovaná restaurace: ${restaurant}`);
}
}
/** /**
* Vrátí menu dané restaurace pro předaný den. * Vrátí menu dané restaurace pro předaný den.
* Pokud neexistuje, provede stažení menu pro příslušný týden a uložení do DB. * Pokud neexistuje, provede stažení menu pro příslušný týden a uložení do DB.
* *
* @param restaurant restaurace * @param restaurant restaurace
* @param date datum, ke kterému získat menu * @param date datum, ke kterému získat menu
* @param mock příznak, zda chceme pouze mock data * @param forceRefresh příznak vynuceného obnovení
*/ */
export async function getRestaurantMenu(restaurant: Restaurant, date?: Date): Promise<RestaurantDayMenu> { export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, forceRefresh = false): Promise<RestaurantDayMenu> {
const usedDate = date ?? getToday(); const usedDate = date ?? getToday();
const dayOfWeekIndex = getDayOfWeekIndex(usedDate); const dayOfWeekIndex = getDayOfWeekIndex(usedDate);
const now = new Date().getTime(); const now = new Date().getTime();
@@ -97,93 +189,56 @@ export async function getRestaurantMenu(restaurant: Restaurant, date?: Date): Pr
} }
let weekMenu = await getMenu(usedDate); let weekMenu = await getMenu(usedDate);
if (weekMenu == null) { weekMenu ??= [{}, {}, {}, {}, {}];
weekMenu = [{}, {}, {}, {}, {}];
}
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
if (weekMenu[i] == null) { weekMenu[i] ??= {};
weekMenu[i] = {}; weekMenu[i][restaurant] ??= {
} lastUpdate: now,
if (weekMenu[i][restaurant] == null) { closed: false,
weekMenu[i][restaurant] = { food: [],
lastUpdate: now, };
closed: false,
food: [],
};
}
} }
if (!weekMenu[dayOfWeekIndex][restaurant]?.food?.length) { if (forceRefresh || !weekMenu[dayOfWeekIndex][restaurant]?.food?.length) {
const firstDay = getFirstWorkDayOfWeek(usedDate); const firstDay = getFirstWorkDayOfWeek(usedDate);
const mock = process.env.MOCK_DATA === 'true';
switch (restaurant) { try {
case 'SLADOVNICKA': const restaurantWeekFood = await fetchRestaurantWeekMenu(restaurant, firstDay);
try {
const sladovnickaFood = await getMenuSladovnicka(firstDay, mock); // Aktualizace menu pro všechny dny
for (let i = 0; i < sladovnickaFood.length; i++) { for (let i = 0; i < restaurantWeekFood.length && i < weekMenu.length; i++) {
weekMenu[i][restaurant]!.food = sladovnickaFood[i]; weekMenu[i][restaurant]!.food = restaurantWeekFood[i];
// Velice chatrný a nespolehlivý způsob detekce uzavření... weekMenu[i][restaurant]!.lastUpdate = now;
if (sladovnickaFood[i].length === 1 && sladovnickaFood[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
// Detekce uzavření pro každou restauraci
switch (restaurant) {
case 'SLADOVNICKA':
if (restaurantWeekFood[i].length === 1 && restaurantWeekFood[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
weekMenu[i][restaurant]!.closed = true; weekMenu[i][restaurant]!.closed = true;
} }
} break;
} catch (e: any) { case 'TECHTOWER':
console.error("Selhalo načtení jídel pro podnik Sladovnická", e); if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name.toLowerCase() === 'svátek') {
}
break;
// case 'UMOTLIKU':
// try {
// const uMotlikuFood = await getMenuUMotliku(firstDay, mock);
// for (let i = 0; i < uMotlikuFood.length; i++) {
// menus[i][restaurant]!.food = uMotlikuFood[i];
// if (uMotlikuFood[i].length === 1 && uMotlikuFood[i][0].name.toLowerCase() === 'zavřeno') {
// menus[i][restaurant]!.closed = true;
// }
// }
// } catch (e: any) {
// console.error("Selhalo načtení jídel pro podnik U Motlíků", e);
// }
// break;
case 'TECHTOWER':
try {
const techTowerFood = await getMenuTechTower(firstDay, mock);
for (let i = 0; i < techTowerFood.length; i++) {
weekMenu[i][restaurant]!.food = techTowerFood[i];
if (techTowerFood[i]?.length === 1 && techTowerFood[i][0].name.toLowerCase() === 'svátek') {
weekMenu[i][restaurant]!.closed = true; weekMenu[i][restaurant]!.closed = true;
} }
} break;
} catch (e: any) { case 'ZASTAVKAUMICHALA':
console.error("Selhalo načtení jídel pro podnik TechTower", e); if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
}
break;
case 'ZASTAVKAUMICHALA':
try {
const zastavkaUmichalaFood = await getMenuZastavkaUmichala(firstDay, mock);
for (let i = 0; i < zastavkaUmichalaFood.length; i++) {
weekMenu[i][restaurant]!.food = zastavkaUmichalaFood[i];
if (zastavkaUmichalaFood[i]?.length === 1 && zastavkaUmichalaFood[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
weekMenu[i][restaurant]!.closed = true; weekMenu[i][restaurant]!.closed = true;
} }
} break;
} catch (e: any) { case 'SENKSERIKOVA':
console.error("Selhalo načtení jídel pro podnik Zastávka u Michala", e); if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name === 'Pro tento den nebylo zadáno menu.') {
}
break;
case 'SENKSERIKOVA':
try {
const senkSerikovaFood = await getMenuSenkSerikova(firstDay, mock);
for (let i = 0; i < senkSerikovaFood.length; i++) {
weekMenu[i][restaurant]!.food = senkSerikovaFood[i];
if (senkSerikovaFood[i]?.length === 1 && senkSerikovaFood[i][0].name === 'Pro tento den nebylo zadáno menu.') {
weekMenu[i][restaurant]!.closed = true; weekMenu[i][restaurant]!.closed = true;
} }
} break;
} catch (e: any) {
console.error("Selhalo načtení jídel pro podnik Pivovarský šenk Šeříková", e);
} }
break; }
// Uložení do storage
await storage.setData(getMenuKey(usedDate), weekMenu);
} catch (e: any) {
console.error(`Selhalo načtení jídel pro podnik ${restaurant}`, e);
} }
await storage.setData(getMenuKey(usedDate), weekMenu);
} }
return weekMenu[dayOfWeekIndex][restaurant]!; return weekMenu[dayOfWeekIndex][restaurant]!;
} }
@@ -326,9 +381,7 @@ export async function addChoice(login: string, trusted: boolean, locationKey: Lu
removeChoiceIfPresent(login, usedDate, locationKey); removeChoiceIfPresent(login, usedDate, locationKey);
} }
// TODO vytáhnout inicializaci "prázdné struktury" do vlastní funkce // TODO vytáhnout inicializaci "prázdné struktury" do vlastní funkce
if (!(data.choices[locationKey])) { data.choices[locationKey] ??= {};
data.choices[locationKey] = {}
}
if (!(login in data.choices[locationKey])) { if (!(login in data.choices[locationKey])) {
if (!data.choices[locationKey]) { if (!data.choices[locationKey]) {
data.choices[locationKey] = {} data.choices[locationKey] = {}

View File

@@ -5,6 +5,12 @@
* Postupem času lze předělat pro efektivnější využití Redis. * Postupem času lze předělat pro efektivnější využití Redis.
*/ */
export interface StorageInterface { export interface StorageInterface {
/**
* Inicializuje úložiště, pokud je potřeba (např. připojení k databázi).
*/
initialize?(): Promise<void>;
/** /**
* Vrátí příznak, zda existují data pro předaný klíč. * Vrátí příznak, zda existují data pro předaný klíč.
* @param key klíč, pro který zjišťujeme data (typicky datum) * @param key klíč, pro který zjišťujeme data (typicky datum)

View File

@@ -5,7 +5,7 @@ import JsonStorage from "./json";
import RedisStorage from "./redis"; import RedisStorage from "./redis";
const ENVIRONMENT = process.env.NODE_ENV ?? 'production'; const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) }); dotenv.config({ path: path.resolve(__dirname, `../../.env.${ENVIRONMENT}`) });
const JSON_KEY = 'json'; const JSON_KEY = 'json';
const REDIS_KEY = 'redis'; const REDIS_KEY = 'redis';
@@ -19,6 +19,13 @@ if (!process.env.STORAGE || process.env.STORAGE?.toLowerCase() === JSON_KEY) {
throw Error("Nepodporovaná hodnota proměnné STORAGE: " + process.env.STORAGE + ", podporované jsou 'json' nebo 'redis'"); throw Error("Nepodporovaná hodnota proměnné STORAGE: " + process.env.STORAGE + ", podporované jsou 'json' nebo 'redis'");
} }
(async () => {
if (storage.initialize) {
await storage.initialize();
}
})();
export default function getStorage(): StorageInterface { export default function getStorage(): StorageInterface {
return storage; return storage;
} }

View File

@@ -11,6 +11,9 @@ export default class RedisStorage implements StorageInterface {
const HOST = process.env.REDIS_HOST ?? 'localhost'; const HOST = process.env.REDIS_HOST ?? 'localhost';
const PORT = process.env.REDIS_PORT ?? 6379; const PORT = process.env.REDIS_PORT ?? 6379;
client = createClient({ url: `redis://${HOST}:${PORT}` }); client = createClient({ url: `redis://${HOST}:${PORT}` });
}
async initialize() {
client.connect(); client.connect();
} }

View File

@@ -29,9 +29,7 @@ export async function getUserVotes(login: string) {
*/ */
export async function updateFeatureVote(login: string, option: FeatureRequest, active: boolean): Promise<VotingData> { export async function updateFeatureVote(login: string, option: FeatureRequest, active: boolean): Promise<VotingData> {
let data = await storage.getData<VotingData>(STORAGE_KEY); let data = await storage.getData<VotingData>(STORAGE_KEY);
if (data == null) { data ??= {};
data = {};
}
if (!(login in data)) { if (!(login in data)) {
data[login] = []; data[login] = [];
} }