diff --git a/.gitignore b/.gitignore
index 462657e..4470b4a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,10 @@ volume/browser
db
downloads
errors
+
+# Web
+web/public
+web/resources/_gen/
+web/assets/jsconfig.json
+web/hugo_stats.json
+web/.hugo_build.lock
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..7d7547b
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "web/themes/PaperMod"]
+ path = web/themes/PaperMod
+ url = https://github.com/adityatelange/hugo-PaperMod.git
diff --git a/Dockerfile b/Dockerfile
index f38a540..543dee9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,6 +10,9 @@ COPY package*.json ./
# Install dependencies
RUN npm install
+# Build
+RUN npm run build
+
# Copy app source code
COPY . .
diff --git a/package.json b/package.json
index 01ce6c3..88c81bf 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,9 @@
"type": "module",
"main": "server.js",
"scripts": {
- "start": "concurrently \"node server.js\" \"node cron-runner.js\""
+ "start": "concurrently \"node server.js\" \"node cron-runner.js\"",
+ "build": "cd web && hugo --gc --minify",
+ "dev-web": "cd web && hugo serve"
},
"dependencies": {
"body-parser": "^2.2.0",
diff --git a/scheduleRules.js b/scheduleRules.js
index eb247f0..c5c6c0f 100644
--- a/scheduleRules.js
+++ b/scheduleRules.js
@@ -1,4 +1,5 @@
// Rules: start and end in 24h format, interval in minutes
+
export const scheduleRules = [
{ start: "0:00", end: "3:00", interval: 180 },
{ start: "3:00", end: "4:00", interval: 60 },
diff --git a/server.js b/server.js
index 15b5580..5b00764 100644
--- a/server.js
+++ b/server.js
@@ -11,13 +11,20 @@ globalThis.File = class File {};
app.use(bodyParser.json());
-app.get('/', async (_, res) => {
- const dataStr = await fs.readFile(path.join(process.cwd(), "db", "current.json"), "utf8");
- const data = JSON.parse(dataStr);
+app.get('/', async (req, res) => {
+ const userAgent = req.headers['user-agent'] || '';
+ const isBrowser = /Mozilla|Chrome|Firefox|Safari|Edg/.test(userAgent);
- data["status"]["currentUpdateSchedule"] = getCurrentInterval();
+ if (isBrowser) {
+ res.sendFile(path.join(process.cwd(), "web", "public", "index.html"));
+ } else {
+ const dataStr = await fs.readFile(path.join(process.cwd(), "db", "current.json"), "utf8");
+ const data = JSON.parse(dataStr);
- res.json(data);
+ data["status"]["currentUpdateSchedule"] = getCurrentInterval();
+
+ res.json(data);
+ }
});
app.get('/versioned/v1', async (_, res) => {
@@ -49,13 +56,9 @@ app.post("/report", async (req, res) => {
return res.status(400).json({ error: "Invalid location value." });
}
-
const url = `https://n8n.local.jzitnik.dev/webhook/${process.env.WEBHOOK_UUID}`;
- console.log(url)
-
let resp;
-
try {
resp = await fetch(url, {
method: "POST",
@@ -77,6 +80,11 @@ app.post("/report", async (req, res) => {
res.status(200).json({ message: "Report received successfully." });
});
+app.use(express.static(path.join(process.cwd(), 'web/public'), {
+ index: 'index.html',
+ extensions: ['html'],
+}));
+
app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`);
});
diff --git a/web/content/posts/api-usage/index.md b/web/content/posts/api-usage/index.md
new file mode 100644
index 0000000..fd27863
--- /dev/null
+++ b/web/content/posts/api-usage/index.md
@@ -0,0 +1,103 @@
+---
+title: "API Dokumentace Ječná Rozvrh"
+date: 2025-12-20
+tags: ["api", "docs"]
+---
+
+Vítejte v dokumentaci pro API systému Ječná Rozvrh. Toto API poskytuje programový přístup k rozvrhům, suplování a dalším informacím.
+
+## Základní Informace
+
+### URL
+Všechny cesty v této dokumentaci jsou relativní k následující základní URL:
+`https://jecnarozvrh.jzitnik.dev/`
+
+### Zastaralý Endpoint: `GET /`
+
+Kořenový endpoint (`/`) je **zastaralý (deprecated)**.
+
+Ačkoliv v současnosti vrací stejná data jako `/versioned/v1`, jeho podpora může být v budoucnu ukončena. **Prosím, nepoužívejte tento endpoint pro nové projekty a existující projekty aktualizujte na verziovaný endpoint.**
+
+---
+
+## Dostupné Verze API
+
+API je verziované, aby byla zajištěna zpětná kompatibilita. Zde je seznam dostupných verzí:
+
+- ### [Verze 1 (v1)](v1)
+ **Status:** Stabilní
+ **Endpoint:** `/versioned/v1`
+
+ Toto je aktuální a doporučená verze API. Klikněte na odkaz pro zobrazení kompletní dokumentace pro v1.
+
+---
+
+## Nezařazené Endpointy
+
+Některé jednodušší endpointy nejsou verziované.
+
+### Stavový Endpoint: `GET /status`
+
+Tento endpoint slouží k rychlé kontrole stavu scraperu, který je zodpovědný za stahování a zpracování dat.
+
+
+Zobrazit možné odpovědi
+
+- **Scraper je v pořádku:**
+ ```json
+ {
+ "working": true
+ }
+ ```
+
+- **Scraper narazil na problém:**
+ ```json
+ {
+ "working": false,
+ "message": "Zde bude podrobnější popis chyby, např. 'Failed to log in'."
+ }
+ ```
+
+
+### Report Endpoint: `POST /report`
+
+Tento endpoint slouží k nahlášení chyby nebo nesrovnalosti v datech.
+
+**Tělo požadavku (Request Body):**
+
+Požadavek musí obsahovat JSON objekt s následujícími poli:
+
+- `class` (string, povinné): Název třídy, které se hlášení týká.
+- `location` (string, povinné): Místo, kde se chyba vyskytla. Povolené hodnoty jsou:
+ - `"TIMETABLE"`
+ - `"ABSENCES"`
+ - `"OTHER"`
+- `content` (string, povinné): Popis problému.
+
+
+Zobrazit příklad těla požadavku
+
+```json
+{
+ "class": "C4a",
+ "location": "TIMETABLE",
+ "content": "V páté hodině chybí předmět, ale má tam být suplování."
+}
+```
+
+
+**Odpovědi (Responses):**
+
+- **`200 OK`**: Hlášení bylo úspěšně přijato.
+ ```json
+ {
+ "message": "Report received successfully."
+ }
+ ```
+- **`400 Bad Request`**: Pokud chybí povinná pole nebo hodnota `location` je neplatná.
+ ```json
+ {
+ "error": "Missing required fields."
+ }
+ ```
+
diff --git a/web/content/posts/api-usage/v1.md b/web/content/posts/api-usage/v1.md
new file mode 100644
index 0000000..262b9e2
--- /dev/null
+++ b/web/content/posts/api-usage/v1.md
@@ -0,0 +1,235 @@
+---
+title: "API Dokumentace - Verze 1"
+date: 2025-12-20
+tags: ["api", "docs", "v1"]
+hiddenInHomelist: true
+---
+
+Tato stránka detailně popisuje **Verzi 1 (v1)** API Ječná Rozvrh.
+
+## Endpoint: `GET /versioned/v1`
+
+Toto je hlavní endpoint, který poskytuje veškerá data o rozvrhu pro v1.
+
+### Struktura Odpovědi
+
+Odpověď je JSON objekt, který obsahuje tři hlavní klíče: `schedule`, `props`, a `status`.
+
+
+Zobrazit příklad struktury odpovědi
+
+```json
+{
+ "schedule": [ /* pole denních rozvrhů */ ],
+ "props": [ /* pole vlastností dnů */ ],
+ "status": { /* objekt stavu */ }
+}
+```
+
+
+---
+
+### Datové Struktury
+
+#### Sekce: `schedule`
+
+Tato sekce je pole, kde každý prvek představuje jeden den. Každý den je objekt, jehož klíče jsou názvy jednotlivých tříd (např. `A1`, `C2a`, `E4`) a speciální klíč `ABSENCE`.
+
+##### Rozvrh Třídy
+- **Klíč:** Název třídy (např. `"A1"`)
+- **Hodnota:** Pole s 10 prvky, které reprezentují 10 vyučovacích hodin.
+ - `string`: Pokud je hodina normálně vyučována, obsahuje název předmětu nebo informaci o změně.
+ - `null`: Pokud hodina odpadá nebo pro ni není záznam.
+ - Text `(bude upřesněno)` může být připojen k předmětu, pokud je změna nejistá.
+
+
+Zobrazit příklad rozvrhu pro třídu A1
+
+```json
+"A1": [
+ "M 5 Kp(Ng)",
+ null,
+ null,
+ "(Me) (bude upřesněno)",
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+]
+```
+
+
+##### Absence Učitelů
+- **Klíč:** `"ABSENCE"`
+- **Hodnota:** 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é.
+ - `teacherCode` (string | null): Zkratka jména učitele (např. "me", "ad").
+ - `type` (string): Typ absence. Může nabývat následujících hodnot:
+ - `"wholeDay"`: Učitel chybí celý den.
+ - `"single"`: Učitel chybí jednu vyučovací hodinu.
+ - `"range"`: Učitel chybí v rozmezí několika hodin.
+ - `"exkurze"`: Učitel je na exkurzi.
+ - `"invalid"`: Záznam o absenci se nepodařilo zpracovat.
+ - `hours` (object | number | null): Specifikuje hodiny absence.
+ - `null`: Pro typy `wholeDay`, `exkurze`, a `invalid`.
+ - `number` (např. `3`): Pro typ `single`.
+ - `object` (např. `{ "from": 2, "to": 4 }`): Pro typ `range`.
+ - `original` (string | null): Pouze pro typ `invalid`, obsahuje původní nezpracovaný text.
+
+
+Zobrazit příklady absencí
+
+**Celý den:**
+```json
+{
+ "teacher": "Jan Novák",
+ "teacherCode": "no",
+ "type": "wholeDay",
+ "hours": null
+}
+```
+
+**Jedna hodina:**
+```json
+{
+ "teacher": "Jan Novák",
+ "teacherCode": "no",
+ "type": "single",
+ "hours": 1
+}
+```
+
+**Rozsah hodin:**
+```json
+{
+ "teacher": "Jan Novák",
+ "teacherCode": "no",
+ "type": "range",
+ "hours": { "from": 2, "to": 4 }
+}
+```
+
+**Exkurze:**
+```json
+{
+ "teacher": "Jan Novák",
+ "teacherCode": "no",
+ "type": "exkurze",
+ "hours": null
+}
+```
+
+**Neplatný záznam:**
+```json
+{
+ "type": "invalid",
+ "teacher": null,
+ "teacherCode": null,
+ "hours": null,
+ "original": "Nezpracovatelný text"
+}
+```
+
+
+#### Sekce: `props` - Vlastnosti Dnů
+
+Pole objektů, které doplňují metadata ke každému dni v poli `schedule`. Pořadí prvků v `props` přesně odpovídá pořadí dnů v `schedule`.
+
+- `date` (string): Datum daného rozvrhu ve formátu `YYYY-MM-DD`.
+- `priprava` (boolean): Hodnota je `true`, pokud je den součástí "přípravného týdne", jinak `false`.
+
+
+Zobrazit příklad props
+
+```json
+"props": [
+ {
+ "date": "2025-12-20",
+ "priprava": false
+ },
+ {
+ "date": "2025-12-21",
+ "priprava": true
+ }
+]
+```
+
+
+#### Sekce: `status` - Stav a Metadata
+
+Objekt poskytující informace o aktuálnosti dat.
+
+- `lastUpdated` (string): Čas poslední úspěšné aktualizace dat ve formátu `HH:MM`.
+- `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).
+
+
+Zobrazit příklad status
+
+```json
+"status": {
+ "lastUpdated": "08:30",
+ "currentUpdateSchedule": 5
+}
+```
+
+
+---
+
+### Kompletní Příklad Odpovědi z `GET /versioned/v1`
+
+
+Zobrazit kompletní příklad
+
+```json
+{
+ "schedule": [
+ {
+ "A1": [
+ "M 6 Kp(Ng)",
+ null,
+ null,
+ null,
+ "(Me) (bude upřesněno)",
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "C2": [
+ "M 6 Kp(Ng)",
+ null,
+ null,
+ null,
+ "(Me) (bude upřesněno)",
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ "ABSENCE": [
+ {
+ "teacher": "Jan Novák",
+ "teacherCode": "no",
+ "type": "range",
+ "hours": {"from": 1, "to": 3}
+ }
+ ]
+ }
+ ],
+ "props": [
+ {
+ "date": "2025-12-20",
+ "priprava": false
+ }
+ ],
+ "status": {
+ "lastUpdated": "14:30",
+ "currentUpdateSchedule": 5
+ }
+}
+```
+
diff --git a/web/content/posts/scraper-documentation/index.md b/web/content/posts/scraper-documentation/index.md
new file mode 100644
index 0000000..ea1a6d8
--- /dev/null
+++ b/web/content/posts/scraper-documentation/index.md
@@ -0,0 +1,14 @@
+---
+title: "Jak Funguje Scraper"
+date: 2025-12-20
+tags: ["scraper", "backend"]
+---
+
+Celý proces je automatizovaný a běží v pravidelných intervalech, které se mění v závislosti na denní době.
+
+1. **Stažení Souboru:** Scraper se pomocí automatizovaného prohlížeče přihlásí na SharePoint SPŠE Ječná, kde je uložen oficiální Excel soubor s mimořádným rozvrhem. Po přihlášení tento soubor stáhne.
+
+2. **Parsování Dat:** Po stažení skript otevře Excel soubor a začne z něj "číst" data. Prochází jednotlivé řádky a sloupce, aby identifikoval rozvrhy pro jednotlivé třídy a informace o absencích učitelů.
+
+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á.
diff --git a/web/content/search.md b/web/content/search.md
new file mode 100644
index 0000000..6a40149
--- /dev/null
+++ b/web/content/search.md
@@ -0,0 +1,5 @@
+---
+title: "Vyhledávání"
+placeholder: Vyhledávejte v dokumentaci...
+layout: "search"
+---
diff --git a/web/hugo.yaml b/web/hugo.yaml
new file mode 100644
index 0000000..a1300ad
--- /dev/null
+++ b/web/hugo.yaml
@@ -0,0 +1,72 @@
+baseURL: https://jecnarozvrh.jzitnik.dev
+title: Ječná Rozvrh API
+theme: ["PaperMod"]
+
+enableInlineShortcodes: true
+enableRobotsTXT: true
+buildDrafts: false
+buildFuture: false
+buildExpired: false
+enableEmoji: true
+pygmentsUseClasses: true
+mainsections: ["posts", "papermod"]
+
+minify:
+ disableXML: true
+ # minifyOutput: true
+
+pagination:
+ disableAliases: false
+ pagerSize: 5
+
+
+menu:
+ main:
+ - name: Vyhledávání
+ url: search/
+ weight: 10
+ - name: Autor
+ url: https://jzitnik.dev
+
+outputs:
+ home:
+ - HTML
+ - RSS
+ - JSON
+
+params:
+ env: production
+ author: Jakub Žitník
+
+ defaultTheme: auto
+ ShowShareButtons: false
+ ShowReadingTime: true
+ displayFullLangName: true
+ ShowPostNavLinks: true
+ ShowBreadCrumbs: true
+ ShowCodeCopyButtons: true
+ ShowRssButtonInSectionTermList: true
+ ShowAllPagesInArchive: true
+ ShowPageNums: true
+ ShowToc: true
+
+ homeInfoParams:
+ Title: "Ječná Rozvrh API"
+ Content: >
+ Oficiální webová stránka SPŠE Ječná Rozvrh API.
+
+ - Ječná Rozvrh API je API pro získávání mimořádného rozvrhu z SPŠE Ječná tabulky.
+
+
+markup:
+ goldmark:
+ renderer:
+ unsafe: true
+ highlight:
+ noClasses: false
+
+services:
+ instagram:
+ disableInlineCSS: true
+ x:
+ disableInlineCSS: true
diff --git a/web/layouts/partials/extend_head.html b/web/layouts/partials/extend_head.html
new file mode 100644
index 0000000..a260963
--- /dev/null
+++ b/web/layouts/partials/extend_head.html
@@ -0,0 +1 @@
+
diff --git a/web/themes/PaperMod b/web/themes/PaperMod
new file mode 160000
index 0000000..7d061d5
--- /dev/null
+++ b/web/themes/PaperMod
@@ -0,0 +1 @@
+Subproject commit 7d061d56d4664bd9c8241eb904994c98b928f0c8