1
0

Compare commits

...

9 Commits

Author SHA1 Message Date
86f71bf178 fix: Some minor fixes
All checks were successful
Remote Deploy / deploy (push) Successful in 3m39s
2026-03-25 08:51:38 +01:00
65998935ce fix: Login taking forever
All checks were successful
Remote Deploy / deploy (push) Successful in 1m30s
2026-03-20 16:16:12 +01:00
570f8f0aaa Merge branch 'docs-refinements'
All checks were successful
Remote Deploy / deploy (push) Successful in 1m27s
2026-03-17 17:08:58 +01:00
75ccf58e82 docs: Fix currentUpdateSchedule is not Optional 2026-03-17 17:06:05 +01:00
d638d67bec docs: Update V3 api docs 2026-03-17 17:06:02 +01:00
12ce6e177f chore: Minor refinements 2026-03-17 12:33:14 +01:00
d8713a5676 fix: Another another fucking teacher absence
All checks were successful
Remote Deploy / deploy (push) Successful in 1m20s
Co-authored-by: Jakub Žitník <email@jzitnik.dev>
2026-03-17 12:06:59 +01:00
8e2cbbaadb chore: Minor changes
All checks were successful
Remote Deploy / deploy (push) Successful in 1m18s
2026-03-15 16:34:11 +01:00
eab7ddf30e fix: Set website template language to czech 2026-03-01 16:58:38 +01:00
12 changed files with 188 additions and 66 deletions

39
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,39 @@
[This same exact file is also in web/content/posts. DO NOT FORGET TO EDIT THAT FILE TOO after editing this one.]: #
# Contributing
Děkuji za váš zájem přispět do tohoto projektu.
Příspěvky jsou vítány, ale **přijímám je pouze e-mailem ve formě `.patch` souborů**.
## Jak přispět
1. Naklonujte repozitář.
2. Vytvořte změny ve vlastní větvi.
3. Vygenerujte patch pomocí `git format-patch`.
Například pro poslední commit:
```bash
git format-patch -1
```
Nebo pro změny vůči main:
```bash
git format-patch origin/main
```
4. Vzniklý .patch soubor pošlete jako přílohu na:
email@jzitnik.dev
# Požadavky na patch
- patch musí být generovaný pomocí git format-patch
- změny musí být jasně popsané v commit message
- jeden patch by měl řešit jednu logickou změnu
- patch musí být aplikovatelný bez konfliktů
# Co se stane potom
- patch zkontroluji
- pokud bude potřeba něco upravit, odpovím e-mailem
- po schválení patch aplikuji do repozitáře

View File

@@ -12,7 +12,7 @@
* GNU General Public License for more details. * GNU General Public License for more details.
*/ */
import ExcelJS, { Worksheet, Cell } from "exceljs" import ExcelJS, { Worksheet } from "exceljs"
import fs from "fs" import fs from "fs"
import parseAbsence, { AbsenceResult } from "../utils/parseAbsence.js" import parseAbsence, { AbsenceResult } from "../utils/parseAbsence.js"
import parseTeachers from "../utils/parseTeachers.js" import parseTeachers from "../utils/parseTeachers.js"
@@ -127,9 +127,6 @@ export default async function parseV1V2(downloadedFilePath: string) {
} }
}) })
// Use an array directly, initialized with nulls or sparse array logic
// The original code used `let final2 = []` but treated it as object `final2[parsedKey] = ...`
// Then `Array.from(final2)` converts it to array.
let final2: (string | null)[] = []; let final2: (string | null)[] = [];
for (const key of allKeys) { for (const key of allKeys) {
@@ -140,7 +137,7 @@ export default async function parseV1V2(downloadedFilePath: string) {
try { try {
const regex = /^úklid\s+(?:\d+\s+)?[A-Za-z]{2}$/; const regex = /^úklid\s+(?:\d+\s+)?[A-Za-z]{2}$/;
const cellText = cell.text || ""; const cellText = cell.text || "";
// @ts-ignore - fgColor is missing in type definition for some versions or intricate structure // @ts-ignore
const fgColor = cell.fill?.fgColor; const fgColor = cell.fill?.fgColor;
if (regex.test(cellText.trim()) || cellText.trim().length == 0 || fgColor === undefined) { if (regex.test(cellText.trim()) || cellText.trim().length == 0 || fgColor === undefined) {
d = false; d = false;

View File

@@ -318,7 +318,7 @@ function extractTakesPlace(sheet: Worksheet) {
const cellTry = sheet.getCell(`${cellTest}${i}`) const cellTry = sheet.getCell(`${cellTest}${i}`)
const cellValue = (typeof cellTry?.value === 'string' ? cellTry.value.trim() : "") || ""; const cellValue = (typeof cellTry?.value === 'string' ? cellTry.value.trim() : "") || "";
if (cellValue.length >= threshold) { if (cellValue.length >= threshold || cellTry.isMerged) {
str += `\n${cellValue}`; str += `\n${cellValue}`;
con = true; con = true;
break; break;
@@ -353,12 +353,16 @@ function extractReservedRooms(sheet: Worksheet) {
cells.forEach((address) => { cells.forEach((address) => {
const row = sheet.getRow(Number(sheet.getCell(address).row)); const row = sheet.getRow(Number(sheet.getCell(address).row));
row.eachCell((cell) => { function numToChar(n: number) {
if (cell.address === address) return; return String.fromCharCode(n + 66);
}
for (let i = 0; i < 10; i++) {
const cell = row.getCell(numToChar(i));
const val = cell.value?.toString().trim(); const val = cell.value?.toString().trim();
result.push(!val || val.length == 0 ? null : val) result.push(!val || val.length == 0 ? null : val)
}); }
}); });
while (result.length < 10) { while (result.length < 10) {
@@ -424,4 +428,4 @@ function formatNowTime() {
); );
} }
//parseV3("db/current.xlsx") //parseV3("volume/db/current.xlsx")

View File

@@ -40,6 +40,9 @@ export interface AbsenceResult {
export const parseSpec = (spec: string | null): Spec | null => { export const parseSpec = (spec: string | null): Spec | null => {
if (!spec) return null; if (!spec) return null;
spec = spec.replace(/\s+/g, "");
let m; let m;
// Handle "6,7" // Handle "6,7"
@@ -118,7 +121,7 @@ export default function parseAbsence(input: string, teacherMap: TeacherMap = {})
// 1. Teachers with specific hours (e.g. "Ab 1-4") // 1. Teachers with specific hours (e.g. "Ab 1-4")
const teacherListThenSpecRe = const teacherListThenSpecRe =
/([A-Za-z]+(?:[,;]\s?[A-Za-z]+)*)(?:\s*)(\d+(?:\+|-\d+|,\d+)?)(?:\.\s*h)?(?![A-Za-z])/g; /([A-Za-z]+(?:[,;]\s?[A-Za-z]+)*)(?:\s*)(\d+(?:\s*\+|\s*-\s*\d+|\s*,\s*\d+)?)(?:\.\s*h)?(?![A-Za-z])/g;
let m; let m;
while ((m = teacherListThenSpecRe.exec(s)) !== null) { while ((m = teacherListThenSpecRe.exec(s)) !== null) {
@@ -157,7 +160,7 @@ export default function parseAbsence(input: string, teacherMap: TeacherMap = {})
} }
// 1b. Teachers with "-exk" followed by spec (e.g. "Ex-exk. 3+") // 1b. Teachers with "-exk" followed by spec (e.g. "Ex-exk. 3+")
const teacherExkWithSpecRe = /([A-Za-z]+)-exk(?:\.)?\s*(\d+(?:\+|-\d+|,\d+)?)/gi; const teacherExkWithSpecRe = /([A-Za-z]+)-exk(?:\.)?\s*(\d+(?:\s*\+|\s*-\s*\d+|\s*,\s*\d+)?)/gi;
while ((m = teacherExkWithSpecRe.exec(s)) !== null) { while ((m = teacherExkWithSpecRe.exec(s)) !== null) {
const matchStart = m.index; const matchStart = m.index;
const matchEnd = teacherExkWithSpecRe.lastIndex; const matchEnd = teacherExkWithSpecRe.lastIndex;
@@ -251,7 +254,7 @@ export default function parseAbsence(input: string, teacherMap: TeacherMap = {})
} }
// 5. Bare specs without teacher → invalid // 5. Bare specs without teacher → invalid
const specOnlyRe = /\b(\d+(?:\+|-\d+|,\d+)?)\b/g; const specOnlyRe = /\b(\d+(?:\s*\+|\s*-\s*\d+|\s*,\s*\d+)?)\b/g;
while ((m = specOnlyRe.exec(s)) !== null) { while ((m = specOnlyRe.exec(s)) !== null) {
const matchStart = m.index; const matchStart = m.index;
if (isConsumed(matchStart)) continue; if (isConsumed(matchStart)) continue;

View File

@@ -37,7 +37,8 @@ const client = wrapper(axios.create({
withCredentials: true, withCredentials: true,
headers: { headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
} }
})); }));

View File

@@ -84,10 +84,6 @@ app.get("/status", async (_: Request, res: Response) => {
} }
}) })
app.get("/posts/viewer/redirect", (_: Request, res: Response) => {
res.redirect(302, "/viewer");
})
app.post("/report", async (req: Request, res: Response): Promise<any> => { app.post("/report", async (req: Request, res: Response): Promise<any> => {
const { class: className, location, content } = req.body; const { class: className, location, content } = req.body;
if (!className || !location || !content) { if (!className || !location || !content) {
@@ -145,6 +141,10 @@ if (SERVE_WEB) {
return handle(req, res) return handle(req, res)
}) })
app.get("/posts/viewer/redirect", (_: Request, res: Response) => {
res.redirect(302, "/viewer");
})
app.use(express.static(path.join(process.cwd(), 'web/public'), { app.use(express.static(path.join(process.cwd(), 'web/public'), {
index: 'index.html', index: 'index.html',
extensions: ['html'], extensions: ['html'],

View File

@@ -286,6 +286,15 @@ test("Ex-exk. 3", [
}, },
]); ]);
test("Su 4 - 6", [
{
teacher: "MUDr. Kristina Studénková",
teacherCode: "su",
type: "range",
hours: {from: 4, to: 6},
}
]);
function test(input: string, expectedOutput: any[]) { function test(input: string, expectedOutput: any[]) {
const res = parseAbsence(input, teachermap); const res = parseAbsence(input, teachermap);

View File

@@ -8,6 +8,8 @@ TocOpen: true
Ječná Rozvrh API má svoji Rust knihovnu pro komunikaci s API. Obsahuje mappings pro Kotlin. Pro další jazyky budou mappingy v budoucnu. Ječná Rozvrh API má svoji Rust knihovnu pro komunikaci s API. Obsahuje mappings pro Kotlin. Pro další jazyky budou mappingy v budoucnu.
[Zdrojový kód knihovny](https://gitea.local.jzitnik.dev/jzitnik/jecna-supl-client)
## Usage ## Usage
### `JecnaSuplClient` struct ### `JecnaSuplClient` struct

View File

@@ -1,19 +1,19 @@
--- ---
title: "API Dokumentace - Verze 2" title: "API Dokumentace - Verze 3"
date: 2026-01-28 date: 2026-03-17
tags: ["api", "docs", "v2"] tags: ["api", "docs", "v3"]
hiddenInHomelist: true hiddenInHomelist: true
--- ---
Tato stránka detailně popisuje **Verzi 2 (v2)** API Ječná Rozvrh. Tato stránka detailně popisuje **Verzi 3 (v3)** API Ječná Rozvrh. Endpoint poskytuje strukturovaná data o suplování, absencích, událostech a rezervacích místností.
## Endpoint: `GET /versioned/v3` ## Endpoint: `GET /versioned/v3`
Toto je hlavní endpoint, který poskytuje veškerá data o rozvrhu pro v2. Toto je hlavní endpoint, který poskytuje veškerá aktuální data.
### Struktura Odpovědi ### Struktura Odpovědi
Odpověď je JSON objekt, který obsahuje dva hlavní klíče: `schedule` a `status`. Odpověď je formátována jako JSON objekt obsahující dva hlavní uzly: `schedule` (samotná data rozvrhu) a `status` (metadata o rozvrhu).
<details> <details>
<summary>Zobrazit příklad struktury odpovědi</summary> <summary>Zobrazit příklad struktury odpovědi</summary>
@@ -21,7 +21,7 @@ Odpověď je JSON objekt, který obsahuje dva hlavní klíče: `schedule` a `sta
```json ```json
{ {
"schedule": { /* objekt denních rozvrhů */ }, "schedule": { /* objekt denních rozvrhů */ },
"status": { /* objekt stavu */ } "status": { /* objekt stavu a metadat */ }
} }
``` ```
</details> </details>
@@ -32,23 +32,31 @@ Odpověď je JSON objekt, který obsahuje dva hlavní klíče: `schedule` a `sta
#### Sekce: `schedule` #### Sekce: `schedule`
Tato sekce je objekt, kde každý klíč představuje datum ve formátu `YYYY-MM-DD` a prvek představuje jeden den. Každý den je objekt, jehož klíče jsou `info`, `changes`, `absence`, `takesPlace` a `reservedRooms` Tato sekce je objekt, kde každý klíč představuje datum ve formátu `YYYY-MM-DD` (např. `"2026-03-17"`). Hodnotou je objekt reprezentující data pro daný den. Tento objekt obsahuje klíče: `info`, `changes`, `absence`, `takesPlace` a `reservedRooms`.
##### `info`
Objekt obsahující dodatečné informace o stavu rozvrhu pro daný den.
- `inWork` (boolean): Nabyde hodnoty `true`, pokud se s daným dnem ze strany školy ještě manipuluje (tzv. "příprava"). Znamená to, že data pro tento den **nemusí být finální** a mohou se ještě měnit.
##### `changes` ##### `changes`
- Objekt
- **Klíč:** Název třídy (např. `"A1"`)
- **Hodnota:** Pole s 10 prvky, které reprezentují 10 vyučovacích hodin.
- Objekt: Pokud je hodina normálně vyučována, obsahuje název předmětu nebo informaci o změně.
- `null`: Pokud pro ni není záznam.
Hodnota je následující objekt Objekt obsahující změny v rozvrhu pro jednotlivé třídy.
- **Klíč:** Název třídy (např. `"A1"`, `"C4a"`).
- **Hodnota:** Pole o **přesně 10 prvcích**, které reprezentují 1. až 10. vyučovací hodinu.
- `null`: Pro danou hodinu není evidována žádná změna (výuka probíhá normálně, nebo hodina není v rozvrhu).
- `Objekt`: Záznam o změně nebo specifické informaci.
Struktura objektu změny:
```ts ```ts
{ {
text: string, text: string, // Textový obsah změny (např. "M 5 odpadá")
backgroundColor?: string, backgroundColor: string | null, // HEX kód barvy pozadí (např. "#DCEDD5")
foregroundColor?: string, foregroundColor?: string, // Volitelný HEX kód barvy textu (např. "#FF000000")
willBeSpecified?: boolean willBeSpecified?: boolean // True, pokud buňka značí "bude upřesněno" (obvykle žlutá barva)
} }
``` ```
@@ -58,7 +66,9 @@ Hodnota je následující objekt
```json ```json
"A1": [ "A1": [
{ {
"text": "M 5 Kp(Ng)" "text": "M 5 Kp(Ng)",
"backgroundColor": "#FFCCCC",
"foregroundColor": "#FF000000"
}, },
null, null,
null, null,
@@ -78,20 +88,26 @@ Hodnota je následující objekt
</details> </details>
##### `absence` ##### `absence`
- Pole objektů, kde každý objekt specifikuje jednu absenci. Struktura objektu je následující:
- `teacher` (string | null): Celé jméno učitele, pokud je známé. Pole objektů, kde každý objekt specifikuje jednu evidovanou absenci učitele nebo změnu jeho stavu (např. exkurze). Pokud pro daný den nikdo nechybí, pole je prázdné `[]`.
- `teacherCode` (string | null): Zkratka jména učitele (např. "me", "ad").
- `type` (string): Typ absence. Může nabývat následujících hodnot: Struktura objektu absence:
- `teacher` (string | null): Celé jméno učitele (např. `"Ing. Jan Novák"`), pokud je známé.
- `teacherCode` (string | null): Interní dvoupísmenná zkratka učitele malými písmeny (např. `"no"`).
- `type` (string): Typ záznamu. Možné hodnoty:
- `"wholeDay"`: Učitel chybí celý den. - `"wholeDay"`: Učitel chybí celý den.
- `"single"`: Učitel chybí jednu vyučovací hodinu. - `"single"`: Učitel chybí jednu konkrétní vyučovací hodinu.
- `"range"`: Učitel chybí v rozmezí několika hodin. - `"range"`: Učitel chybí v rozmezí několika vyučovacích hodin.
- `"exkurze"`: Učitel je na exkurzi. - `"exkurze"`: Učitel je mimo školu na exkurzi.
- `"zastoupen"`: Specifický stav, kdy učitele kompletně zastupuje někdo jiný.
- `"invalid"`: Záznam o absenci se nepodařilo zpracovat. - `"invalid"`: Záznam o absenci se nepodařilo zpracovat.
- `hours` (object | number | null): Specifikuje hodiny absence. - `hours` (object | number | null): Specifikuje vyučovací hodiny, kterých se záznam týká.
- `null`: Pro typy `wholeDay`, `exkurze`, a `invalid`. - `null`: Pro typy `wholeDay`, `zastoupen`, `invalid` a často i `exkurze` (pokud je na celý den).
- `number` (např. `3`): Pro typ `single`. - `number` (např. `3`): Pro typ `single` (týká se pouze 3. hodiny).
- `object` (např. `{ "from": 2, "to": 4 }`): Pro typ `range`. - `object` (např. `{ "from": 2, "to": 4 }`): Pro typ `range` nebo částečnou `exkurze` (týká se času od 2. do 4. hodiny).
- `original` (string | null): Pouze pro typ `invalid`, obsahuje původní nezpracovaný text. - `zastupuje` (object | volitelné): Přítomno **pouze** u typu `"zastoupen"`. Obsahuje vnořené klíče `teacher` a `teacherCode` pro učitele, který přebírá výuku.
- `original` (string | volitelné): Přítomno **pouze** u typu `"invalid"`. Obsahuje původní textový řetězec, který se nepodařilo zpracovat.
<details> <details>
<summary>Zobrazit příklady absencí</summary> <summary>Zobrazit příklady absencí</summary>
@@ -116,27 +132,27 @@ Hodnota je následující objekt
} }
``` ```
**Rozsah hodin:** **Rozsah hodin (od-do):**
```json ```json
{ {
"teacher": "Jan Novák", "teacher": "Petr Svoboda",
"teacherCode": "no", "teacherCode": "sv",
"type": "range", "type": "range",
"hours": { "from": 2, "to": 4 } "hours": { "from": 2, "to": 4 }
} }
``` ```
**Exkurze:** **Exkurze: (celý den)**
```json ```json
{ {
"teacher": "Jan Novák", "teacher": "Bc. Jakub Dvořák",
"teacherCode": "no", "teacherCode": "dv",
"type": "exkurze", "type": "exkurze",
"hours": null "hours": null
} }
``` ```
**Zastupuje:** **Kompletní zastoupení:**
```json ```json
{ {
"teacher": "Ing. Zdeněk Vondra", "teacher": "Ing. Zdeněk Vondra",
@@ -145,9 +161,9 @@ Hodnota je následující objekt
"hours": null, "hours": null,
"zastupuje": { "zastupuje": {
"teacher": "David Janoušek", "teacher": "David Janoušek",
"teacherCode": "jk", "teacherCode": "jk"
}, }
}, }
``` ```
**Neplatný záznam:** **Neplatný záznam:**
@@ -164,22 +180,24 @@ Hodnota je následující objekt
##### `takesPlace` ##### `takesPlace`
String obsahující aktuálně probíhající akce ten den. String obsahující text aktuálně probíhajících akcí pro daný den (např. akce, exkurze, maturitní zkoušky).
#### `reservedRooms` - Může obsahovat znaky nového řádku `\n` pro oddělení více akcí.
- Pokud pro daný den žádná akce neprobíhá, vrací prázdný string `""`.
Pole 10 prvků (string | null) pro jakou hodinu jsou rezervované jaké místnosti. ##### `reservedRooms`
#### `info.inWork` Pole o **přesně 10 prvcích**, které reprezentuje rezervace konkrétních místností pro 1. až 10. vyučovací hodinu.
Boolean jestli je daná tabulka in work (příprava). - `string`: Název rezervované místnosti (např. `"19a"`, `"TV"`).
- `null`: Pokud pro danou hodinu není evidována žádná rezervace místnosti.
#### Sekce: `status` - Stav a Metadata #### Sekce: `status` - Stav a Metadata
Objekt poskytující informace o aktuálnosti dat. Objekt poskytující informace o aktuálnosti dat.
- `lastUpdated` (string): Čas posled úspěšné aktualizace dat ve formátu `HH:MM`. - `lastUpdated` (string): Čas (ve formátu `HH:MM`), kdy byla data naposledy úspěšně získána a uložena na serveru.
- `currentUpdateSchedule` (number): Interval v **minutách**, ve kterém scraper interně kontroluje a stahuje novou verzi rozvrhu. Tento interval se dynamicky mění v závislosti na denní době (kratší během vyučování, delší v noci). - `currentUpdateSchedule` (number): Interval v minutách, ve kterém systém periodicky kontroluje nová data (může se měnit v závislosti na denní době - kratší během vyučování, delší v noci).
<details> <details>
<summary>Zobrazit příklad status</summary> <summary>Zobrazit příklad status</summary>

View File

@@ -0,0 +1,44 @@
---
title: "Přispívání do projektu"
date: 2026-03-17
tags: ["code", "contribution"]
hiddenInHomelist: true
---
# Contributing
Děkuji za váš zájem přispět do tohoto projektu.
Příspěvky jsou vítány, ale **přijímám je pouze e-mailem ve formě `.patch` souborů**.
## Jak přispět
1. Naklonujte repozitář.
2. Vytvořte změny ve vlastní větvi.
3. Vygenerujte patch pomocí `git format-patch`.
Například pro poslední commit:
```bash
git format-patch -1
```
Nebo pro změny vůči main:
```bash
git format-patch origin/main
```
4. Vzniklý .patch soubor pošlete jako přílohu na:
email@jzitnik.dev
# Požadavky na patch
- patch musí být generovaný pomocí git format-patch
- změny musí být jasně popsané v commit message
- jeden patch by měl řešit jednu logickou změnu
- patch musí být aplikovatelný bez konfliktů
# Co se stane potom
- patch zkontroluji
- pokud bude potřeba něco upravit, odpovím e-mailem
- po schválení patch aplikuji do repozitáře

View File

@@ -12,3 +12,7 @@ Celý proces je automatizovaný a běží v pravidelných intervalech, které se
3. **Generování JSONu:** Všechna přečtená a zpracovaná data jsou následně uložena do jednoho souboru ve formátu JSON. 3. **Generování JSONu:** Všechna přečtená a zpracovaná data jsou následně uložena do jednoho souboru ve formátu JSON.
4. **Poskytnutí přes API:** Tento JSON soubor je finálním zdrojem dat, který API server používá. 4. **Poskytnutí přes API:** Tento JSON soubor je finálním zdrojem dat, který API server používá.
---
[Kontribuce](/posts/contributing)

View File

@@ -10,6 +10,7 @@ buildExpired: false
enableEmoji: true enableEmoji: true
pygmentsUseClasses: true pygmentsUseClasses: true
mainsections: ["posts", "papermod"] mainsections: ["posts", "papermod"]
defaultContentLanguage: "cs"
minify: minify:
disableXML: true disableXML: true