@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 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
- - `title`: Planname
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-mcp.git
228
- cd dsbmobile-mcp
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 HTML page)
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, title: string, lastUpdated: string, url: string): SubstitutionPlan;
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 HTML page)
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 plans = [];
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.title, timetable.date, timetable.url);
236
- plans.push(plan);
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 plans;
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, title, lastUpdated, url) {
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 planDate = dateMatch ? decodeHtmlEntities(stripHtmlTags(dateMatch[1])) : '';
291
- // Extract affected classes from info table
292
- let affectedClasses = '';
293
- const infoRows = html.matchAll(/<tr class="info"><td[^>]*>(.*?)<\/td><td[^>]*>(.*?)<\/td><\/tr>/gs);
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 { title, planDate, lastUpdated, url, affectedClasses, entries };
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 page), each containing:
16
- - title: Plan name (e.g., "V-Homepage heute - subst_001 (Seite 1)")
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.title}`, `**Date**: ${plan.planDate}`, `**Last Updated**: ${plan.lastUpdated}`);
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 fully parsed substitution plan page
78
+ * A substitution plan for a single date, merging all pages for that day
79
79
  */
80
80
  export interface SubstitutionPlan {
81
- /** Plan title including date (e.g., "V-Homepage heute - subst_001") */
82
- title: string;
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
- /** URL the plan was fetched from */
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.1.0",
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-mcp.git"
7
+ "url": "https://github.com/udondan/dsbmobile.git"
8
8
  },
9
9
  "author": {
10
10
  "name": "Daniel Schroeder",