1
0

feat: Announcement API
Remote Deploy / deploy (push) Successful in 2m38s

This commit is contained in:
2026-06-02 11:18:49 +02:00
parent 939633b675
commit abddc62f8c
5 changed files with 70 additions and 13 deletions
+5
View File
@@ -2,5 +2,10 @@ EMAIL=username@spsejecna.cz
PASSWORD=mojesupertajneheslo PASSWORD=mojesupertajneheslo
SHAREPOINT_URL=https://spsejecnacz.sharepoint.com/:x:/s/nastenka/ESy19K245Y9BouR5ksciMvgBu3Pn_9EaT0fpP8R6MrkEmg SHAREPOINT_URL=https://spsejecnacz.sharepoint.com/:x:/s/nastenka/ESy19K245Y9BouR5ksciMvgBu3Pn_9EaT0fpP8R6MrkEmg
# API for announcing errors and other global stuff for the client
# This server is open-source too, tho I don't recommend hosting it yourself,
# since it is used for global announcements and if you host it yourself, you won't get the latest news.
ANNOUNCEMENT_API=https://announcement.jecnarozvrh.jzitnik.dev
# For the viewer # For the viewer
API_URl=http://localhost:3000 API_URl=http://localhost:3000
+4
View File
@@ -9,5 +9,9 @@ services:
NODE_ENV: production NODE_ENV: production
EMAIL: username@spsejecna.cz EMAIL: username@spsejecna.cz
PASSWORD: mojesupertajneheslo PASSWORD: mojesupertajneheslo
# API for announcing errors and other global stuff for the client
# This server is open-source too, tho I don't recommend hosting it yourself,
# since it is used for global announcements and if you host it yourself, you won't get the latest news.
ANNOUNCEMENT_API: https://announcement.jecnarozvrh.jzitnik.dev
volumes: volumes:
- ./volume:/usr/src/app/volume - ./volume:/usr/src/app/volume
+41
View File
@@ -0,0 +1,41 @@
const API_BASE_URL = process.env.ANNOUNCEMENT_API || "http://localhost:3000";
export type Announcement = {
id: number;
text_content: string;
author: string;
flags: Flag[];
start_date: string;
end_date: string;
};
export enum Flag {
SHOW_ALL_ENTRIES
}
export type AnnouncementResponse = {
[date: string]: Announcement[];
}
export default async function getAnnouncements(dates: string[]): Promise<AnnouncementResponse> {
if (dates.length === 0) {
return {};
}
const url = new URL(`/v1/announcements/${dates.join(",")}`, API_BASE_URL).toString();
try {
const response = await fetch(url);
const data = await response.json();
if (!response.ok) {
return {};
}
return data;
} catch (e) {
console.error("Some random ahh error:", e);
return {};
}
}
+6 -4
View File
@@ -1,4 +1,4 @@
import fs from "fs" import fs from "fs/promises"
const CLASSES: string[] = [ const CLASSES: string[] = [
"A1a", "A1b", "A1c", "C1a", "C1b", "C1c", "A2", "C2a", "C2b", "C2c", "E2", "C3a", "C3b", "C3c", "E3" "A1a", "A1b", "A1c", "C1a", "C1b", "C1c", "A2", "C2a", "C2b", "C2c", "E2", "C3a", "C3b", "C3c", "E3"
@@ -45,7 +45,7 @@ interface ScheduleData {
status: { lastUpdated: string }; status: { lastUpdated: string };
} }
export default function generateArchivedV1_V2(): void { export default async function generateArchivedV1_V2() {
const dates = getCurrentWeekMondayToFriday(); const dates = getCurrentWeekMondayToFriday();
const currentDate = new Date(); const currentDate = new Date();
const lastUpdated = currentDate.getHours().toString().padStart(2, "0") + ":" + currentDate.getMinutes().toString().padStart(2, "0"); const lastUpdated = currentDate.getHours().toString().padStart(2, "0") + ":" + currentDate.getMinutes().toString().padStart(2, "0");
@@ -73,6 +73,8 @@ export default function generateArchivedV1_V2(): void {
data.schedule.push(d); data.schedule.push(d);
} }
fs.writeFileSync("volume/db/v1.json", JSON.stringify(data, null, 2)); await Promise.all([
fs.writeFileSync("volume/db/v2.json", JSON.stringify(data, null, 2)); fs.writeFile("volume/db/v1.json", JSON.stringify(data, null, 2)),
fs.writeFile("volume/db/v2.json", JSON.stringify(data, null, 2)),
]);
} }
+14 -9
View File
@@ -18,6 +18,7 @@ import parseTeachers from "../utils/parseTeachers.js"
import ExcelJS, { Worksheet, Cell, Row, Workbook } from "exceljs" import ExcelJS, { Worksheet, Cell, Row, Workbook } from "exceljs"
import JSZip from "jszip"; import JSZip from "jszip";
import { parseStringPromise } from "xml2js"; import { parseStringPromise } from "xml2js";
import getAnnouncements, { Flag } from "../api/annoucements.js";
interface ThemeColors { interface ThemeColors {
[key: number]: string | null; [key: number]: string | null;
@@ -123,11 +124,13 @@ export default async function parseV3(workbook: Workbook, downloadedFilePath: st
const upcoming = getUpcomingSheets(workbook); const upcoming = getUpcomingSheets(workbook);
const resolvedDays = groupSheetsByDate(upcoming); const resolvedDays = groupSheetsByDate(upcoming);
const getDates = resolvedDays.map((d) => d.dateKey);
const annoucements = await getAnnouncements(getDates);
const schedule: any = {}; const schedule: any = {};
for (const { dateKey, sheet } of resolvedDays) { for (const { dateKey, sheet } of resolvedDays) {
const { changes, absence, inWork, takesPlace, reservedRooms } = extractDaySchedule(sheet, teacherMap, themeColors); const { changes, absence, inWork, takesPlace, reservedRooms } = extractDaySchedule(sheet, teacherMap, themeColors, annoucements[dateKey].map(a => a.flags).flat());
schedule[dateKey] = { schedule[dateKey] = {
info: { inWork }, info: { inWork },
@@ -140,6 +143,7 @@ export default async function parseV3(workbook: Workbook, downloadedFilePath: st
const data = { const data = {
status: { lastUpdated: formatNowTime() }, status: { lastUpdated: formatNowTime() },
annoucements,
schedule, schedule,
}; };
@@ -202,9 +206,9 @@ function groupSheetsByDate(items: ResolvedDay[]) {
// ──────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────
// //
function extractDaySchedule(sheet: Worksheet, teacherMap: Record<string, string>, themeColors: ThemeColors | null) { function extractDaySchedule(sheet: Worksheet, teacherMap: Record<string, string>, themeColors: ThemeColors | null, flags: Flag[]) {
return { return {
changes: extractClassChanges(sheet, themeColors), changes: extractClassChanges(sheet, themeColors, flags),
absence: extractAbsence(sheet, teacherMap), absence: extractAbsence(sheet, teacherMap),
inWork: isPripravaSheet(sheet.name.toLowerCase()), inWork: isPripravaSheet(sheet.name.toLowerCase()),
takesPlace: extractTakesPlace(sheet), takesPlace: extractTakesPlace(sheet),
@@ -226,7 +230,8 @@ function isPripravaSheet(name: string) {
// ──────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────
// //
function extractClassChanges(sheet: Worksheet, themeColors: ThemeColors | null) { function extractClassChanges(sheet: Worksheet, themeColors: ThemeColors | null, flags: Flag[]) {
const ignoreColors = flags.includes(Flag.SHOW_ALL_ENTRIES)
const classRegex = /[AEC][0-4][a-c]?\s*\/.*/s; const classRegex = /[AEC][0-4][a-c]?\s*\/.*/s;
const prefixRegex = /[AEC][0-4][a-c]?/; const prefixRegex = /[AEC][0-4][a-c]?/;
@@ -248,20 +253,20 @@ function extractClassChanges(sheet: Worksheet, themeColors: ThemeColors | null)
classCells.forEach((address, index) => { classCells.forEach((address, index) => {
const row = sheet.getRow(Number(sheet.getCell(address).row)); const row = sheet.getRow(Number(sheet.getCell(address).row));
changes[classes[index]] = buildLessonArray(row, address, themeColors); changes[classes[index]] = buildLessonArray(row, address, themeColors, ignoreColors);
}); });
return changes; return changes;
} }
function buildLessonArray(row: Row, ignoreAddress: string, themeColors: ThemeColors | null) { function buildLessonArray(row: Row, ignoreAddress: string, themeColors: ThemeColors | null, ignoreColors: boolean) {
const lessons: (Lesson | null)[] = []; const lessons: (Lesson | null)[] = [];
row.eachCell((cell) => { row.eachCell((cell) => {
if (cell.address === ignoreAddress) return; if (cell.address === ignoreAddress) return;
const colIndex = letterToNumber(cell.address.replace(/[0-9]/g, "")); const colIndex = letterToNumber(cell.address.replace(/[0-9]/g, ""));
lessons[colIndex] = parseLessonCell(cell, themeColors); lessons[colIndex] = parseLessonCell(cell, themeColors, ignoreColors);
}); });
const normalized = Array.from(lessons, (x) => (x === undefined ? null : x)); const normalized = Array.from(lessons, (x) => (x === undefined ? null : x));
@@ -270,13 +275,13 @@ function buildLessonArray(row: Row, ignoreAddress: string, themeColors: ThemeCol
return normalized.slice(1, 11); return normalized.slice(1, 11);
} }
function parseLessonCell(cell: Cell, themeColors: ThemeColors | null): Lesson | null { function parseLessonCell(cell: Cell, themeColors: ThemeColors | null, ignoreColors: boolean): Lesson | null {
try { try {
const text = (cell.text || "").trim(); const text = (cell.text || "").trim();
const cleanupRegex = /^úklid\s+(?:\d+\s+)?[A-Za-z]{2}$/; const cleanupRegex = /^úklid\s+(?:\d+\s+)?[A-Za-z]{2}$/;
// @ts-ignore // @ts-ignore
if (!text || cleanupRegex.test(text) || !cell.fill?.fgColor) return null; if (!text || cleanupRegex.test(text) || (!cell.fill?.fgColor && !ignoreColors)) return null;
const backgroundColor = resolveCellColor(cell, themeColors); const backgroundColor = resolveCellColor(cell, themeColors);
const foregroundColor = !backgroundColor ? undefined : ( const foregroundColor = !backgroundColor ? undefined : (