@udondan/dsbmobile 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -6
- package/dist/services/dsbmobile.d.ts +2 -2
- package/dist/services/dsbmobile.js +19 -18
- package/dist/tools/substitutions.js +3 -9
- package/dist/types.d.ts +4 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -183,12 +183,10 @@ Lädt und parst alle Vertretungsplan-Seiten und gibt strukturierte Einträge zur
|
|
|
183
183
|
|
|
184
184
|
- `className` (optional): Klassenfilter, z. B. `07b` oder `Q2_Kra`. Groß-/Kleinschreibung wird ignoriert. Standardmäßig wird `DSB_CLASS` verwendet, falls gesetzt.
|
|
185
185
|
|
|
186
|
-
**Rückgabe**: Liste von Plänen, jeweils mit:
|
|
186
|
+
**Rückgabe**: Liste von Plänen (ein Objekt pro Tag), jeweils mit:
|
|
187
187
|
|
|
188
|
-
- `
|
|
189
|
-
- `planDate`: Datum laut Plan (z. B. „20.3.2026 Freitag (Seite 1 / 8)")
|
|
188
|
+
- `date`: Datum im ISO-Format (z. B. `2026-03-20`)
|
|
190
189
|
- `lastUpdated`: Zeitstempel der letzten Aktualisierung
|
|
191
|
-
- `affectedClasses`: Kommagetrennte Liste betroffener Klassen
|
|
192
190
|
- `entries`: Liste der Vertretungseinträge, jeweils mit:
|
|
193
191
|
- `className`: Klasse (z. B. `07b`, `Q2_Kra`)
|
|
194
192
|
- `type`: Art der Vertretung (z. B. `Vertretung`, `Statt-Vertretung`, `Entfall`)
|
|
@@ -224,8 +222,8 @@ Listet alle verfügbaren Dokumente und Dateien auf.
|
|
|
224
222
|
|
|
225
223
|
```bash
|
|
226
224
|
# Repository klonen
|
|
227
|
-
git clone https://github.com/udondan/dsbmobile
|
|
228
|
-
cd dsbmobile
|
|
225
|
+
git clone https://github.com/udondan/dsbmobile.git
|
|
226
|
+
cd dsbmobile
|
|
229
227
|
|
|
230
228
|
# Abhängigkeiten installieren
|
|
231
229
|
bun install
|
|
@@ -64,7 +64,7 @@ export declare class DsbmobileClient {
|
|
|
64
64
|
* This fetches the actual HTML content of each timetable page and parses the
|
|
65
65
|
* substitution table into structured data.
|
|
66
66
|
*
|
|
67
|
-
* @returns Array of parsed substitution plans (one per
|
|
67
|
+
* @returns Array of parsed substitution plans (one per calendar day, pages merged)
|
|
68
68
|
* @throws Error if authentication fails or the request fails
|
|
69
69
|
*/
|
|
70
70
|
getSubstitutions(): Promise<SubstitutionPlan[]>;
|
|
@@ -78,4 +78,4 @@ export declare class DsbmobileClient {
|
|
|
78
78
|
* The HTML is generated by Untis and has a consistent structure.
|
|
79
79
|
* Exported for unit testing.
|
|
80
80
|
*/
|
|
81
|
-
export declare function parseSubstitutionHtml(html: string,
|
|
81
|
+
export declare function parseSubstitutionHtml(html: string, lastUpdated: string): SubstitutionPlan;
|
|
@@ -215,13 +215,13 @@ export class DsbmobileClient {
|
|
|
215
215
|
* This fetches the actual HTML content of each timetable page and parses the
|
|
216
216
|
* substitution table into structured data.
|
|
217
217
|
*
|
|
218
|
-
* @returns Array of parsed substitution plans (one per
|
|
218
|
+
* @returns Array of parsed substitution plans (one per calendar day, pages merged)
|
|
219
219
|
* @throws Error if authentication fails or the request fails
|
|
220
220
|
*/
|
|
221
221
|
async getSubstitutions() {
|
|
222
222
|
try {
|
|
223
223
|
const timetables = await this.getTimetables();
|
|
224
|
-
const
|
|
224
|
+
const merged = new Map();
|
|
225
225
|
for (const timetable of timetables) {
|
|
226
226
|
try {
|
|
227
227
|
// Use a standalone axios call with the full URL (not the base-URL-bound instance)
|
|
@@ -232,14 +232,20 @@ export class DsbmobileClient {
|
|
|
232
232
|
});
|
|
233
233
|
// The HTML is encoded in iso-8859-1/windows-1252 — decode it properly
|
|
234
234
|
const htmlText = new TextDecoder('windows-1252').decode(response.data);
|
|
235
|
-
const plan = parseSubstitutionHtml(htmlText, timetable.
|
|
236
|
-
|
|
235
|
+
const plan = parseSubstitutionHtml(htmlText, timetable.date);
|
|
236
|
+
const existing = merged.get(plan.date);
|
|
237
|
+
if (existing) {
|
|
238
|
+
existing.entries.push(...plan.entries);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
merged.set(plan.date, { ...plan, entries: [...plan.entries] });
|
|
242
|
+
}
|
|
237
243
|
}
|
|
238
244
|
catch {
|
|
239
245
|
// Skip pages that fail to load; don't abort the whole request
|
|
240
246
|
}
|
|
241
247
|
}
|
|
242
|
-
return
|
|
248
|
+
return [...merged.values()];
|
|
243
249
|
}
|
|
244
250
|
catch (error) {
|
|
245
251
|
if (error instanceof Error && error.message.startsWith('Error:')) {
|
|
@@ -284,20 +290,15 @@ export class DsbmobileClient {
|
|
|
284
290
|
* The HTML is generated by Untis and has a consistent structure.
|
|
285
291
|
* Exported for unit testing.
|
|
286
292
|
*/
|
|
287
|
-
export function parseSubstitutionHtml(html,
|
|
288
|
-
// Extract plan date (e.g. "20.3.2026 Freitag (Seite 1 / 8)")
|
|
293
|
+
export function parseSubstitutionHtml(html, lastUpdated) {
|
|
294
|
+
// Extract plan date (e.g. "20.3.2026 Freitag (Seite 1 / 8)") and convert to ISO date string
|
|
289
295
|
const dateMatch = /class="mon_title">(.*?)<\/div>/.exec(html);
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
for (const match of infoRows) {
|
|
295
|
-
const label = decodeHtmlEntities(stripHtmlTags(match[1]));
|
|
296
|
-
const value = decodeHtmlEntities(stripHtmlTags(match[2]));
|
|
297
|
-
if (label.toLowerCase().includes('klassen')) {
|
|
298
|
-
affectedClasses = value;
|
|
299
|
-
}
|
|
296
|
+
const rawDate = dateMatch ? decodeHtmlEntities(stripHtmlTags(dateMatch[1])) : '';
|
|
297
|
+
const datePartMatch = /^(\d{1,2})\.(\d{1,2})\.(\d{4})/.exec(rawDate);
|
|
298
|
+
if (!datePartMatch) {
|
|
299
|
+
throw new Error(`Could not parse plan date from HTML: "${rawDate}"`);
|
|
300
300
|
}
|
|
301
|
+
const date = `${datePartMatch[3]}-${datePartMatch[2].padStart(2, '0')}-${datePartMatch[1].padStart(2, '0')}`;
|
|
301
302
|
// Parse all table rows
|
|
302
303
|
const entries = [];
|
|
303
304
|
let currentClass = '';
|
|
@@ -337,5 +338,5 @@ export function parseSubstitutionHtml(html, title, lastUpdated, url) {
|
|
|
337
338
|
text: text === '\u00A0' ? '' : text,
|
|
338
339
|
});
|
|
339
340
|
}
|
|
340
|
-
return {
|
|
341
|
+
return { date, lastUpdated, entries };
|
|
341
342
|
}
|
|
@@ -12,11 +12,9 @@ export function registerSubstitutionsTool(server, client) {
|
|
|
12
12
|
description: `Fetches and parses the actual substitution plan (Vertretungsplan) pages from DSBmobile,
|
|
13
13
|
returning structured substitution entries for each class.
|
|
14
14
|
|
|
15
|
-
Returns a list of substitution plans (one per
|
|
16
|
-
-
|
|
17
|
-
- planDate: Date shown on the plan (e.g., "20.3.2026 Freitag (Seite 1 / 8)")
|
|
15
|
+
Returns a list of substitution plans (one per day), each containing:
|
|
16
|
+
- date: ISO date string (e.g., "2026-03-20")
|
|
18
17
|
- lastUpdated: When the plan was last updated
|
|
19
|
-
- affectedClasses: Comma-separated list of affected class names
|
|
20
18
|
- entries: Array of substitution entries, each with:
|
|
21
19
|
- className: The class (e.g., "05b", "Q2_Kra")
|
|
22
20
|
- type: Substitution type (e.g., "Vertretung", "Statt-Vertretung", "Entfall")
|
|
@@ -126,11 +124,7 @@ function formatSubstitutions(plans, filter) {
|
|
|
126
124
|
for (const plan of plans) {
|
|
127
125
|
if (plan.entries.length === 0)
|
|
128
126
|
continue;
|
|
129
|
-
lines.push(`## ${plan.
|
|
130
|
-
if (plan.affectedClasses) {
|
|
131
|
-
lines.push(`**Affected Classes**: ${plan.affectedClasses}`);
|
|
132
|
-
}
|
|
133
|
-
lines.push('');
|
|
127
|
+
lines.push(`## ${plan.date}`, `**Last Updated**: ${plan.lastUpdated}`, '');
|
|
134
128
|
// Group entries by class
|
|
135
129
|
const byClass = new Map();
|
|
136
130
|
for (const entry of plan.entries) {
|
package/dist/types.d.ts
CHANGED
|
@@ -75,20 +75,14 @@ export interface SubstitutionEntry {
|
|
|
75
75
|
text: string;
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
|
-
* A
|
|
78
|
+
* A substitution plan for a single date, merging all pages for that day
|
|
79
79
|
*/
|
|
80
80
|
export interface SubstitutionPlan {
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
/** Date string from the plan (e.g., "20.3.2026 Freitag (Seite 1 / 8)") */
|
|
84
|
-
planDate: string;
|
|
81
|
+
/** ISO date string (e.g., "2026-03-20") */
|
|
82
|
+
date: string;
|
|
85
83
|
/** Last updated timestamp */
|
|
86
84
|
lastUpdated: string;
|
|
87
|
-
/**
|
|
88
|
-
url: string;
|
|
89
|
-
/** Affected classes */
|
|
90
|
-
affectedClasses: string;
|
|
91
|
-
/** All substitution entries */
|
|
85
|
+
/** All substitution entries for this date */
|
|
92
86
|
entries: SubstitutionEntry[];
|
|
93
87
|
}
|
|
94
88
|
/**
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@udondan/dsbmobile",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "SDK, CLI, and MCP server for DSBmobile — access German school substitution plans (Vertretungspläne)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "https://github.com/udondan/dsbmobile
|
|
7
|
+
"url": "https://github.com/udondan/dsbmobile.git"
|
|
8
8
|
},
|
|
9
9
|
"author": {
|
|
10
10
|
"name": "Daniel Schroeder",
|