@wilm-ai/wilma-client 0.0.4 → 1.1.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/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Exam, Message, MessageFolder, NewsItem, WilmaProfile, StudentInfo } from "./types.js";
1
+ import type { Exam, Message, MessageFolder, NewsItem, OverviewData, WilmaProfile, StudentInfo } from "./types.js";
2
2
  export declare class WilmaClient {
3
3
  private session;
4
4
  private constructor();
@@ -18,4 +18,7 @@ export declare class WilmaClient {
18
18
  end?: string;
19
19
  }) => Promise<Exam[]>;
20
20
  };
21
+ overview: {
22
+ get: () => Promise<OverviewData>;
23
+ };
21
24
  }
package/dist/client.js CHANGED
@@ -3,6 +3,7 @@ import { parseWilmaTimestamp } from "./parsers/dates.js";
3
3
  import { parseMessagesList, parseMessageDetailHtml } from "./parsers/messages.js";
4
4
  import { parseNewsDetailHtml, parseNewsDetailJson, parseNewsList, parseNewsListHtml, } from "./parsers/news.js";
5
5
  import { parseExamsHtml } from "./parsers/exams.js";
6
+ import { parseOverview } from "./parsers/overview.js";
6
7
  import { parseStudentsFromHome } from "./parsers/students.js";
7
8
  export class WilmaClient {
8
9
  session;
@@ -87,13 +88,27 @@ export class WilmaClient {
87
88
  };
88
89
  exams = {
89
90
  list: async (opts) => {
90
- const startQs = opts?.start ? `?start=${opts.start}` : "";
91
- const endQs = opts?.end ? `&end=${opts.end}` : "";
92
- const resp = await this.session.get(`/exams/calendar${startQs}${endQs}`);
91
+ const params = new URLSearchParams();
92
+ if (opts?.start) {
93
+ params.set("start", opts.start);
94
+ }
95
+ if (opts?.end) {
96
+ params.set("end", opts.end);
97
+ }
98
+ const query = params.toString();
99
+ const path = query ? `/exams/calendar?${query}` : "/exams/calendar";
100
+ const resp = await this.session.get(path);
93
101
  const text = await resp.text();
94
102
  return parseExamsHtml(text);
95
103
  },
96
104
  };
105
+ overview = {
106
+ get: async () => {
107
+ const resp = await this.session.get("/overview");
108
+ const text = await resp.text();
109
+ return parseOverview(safeJson(text));
110
+ },
111
+ };
97
112
  }
98
113
  function safeJson(text) {
99
114
  try {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { Exam, Message, MessageFolder, Municipality, NewsItem, TenantDiscoveryResponse, TenantInfo, WilmaProfile, StudentInfo, } from "./types.js";
1
+ export type { Exam, ExamGrade, HomeworkItem, Message, MessageFolder, Municipality, NewsItem, OverviewData, ScheduleLesson, TenantDiscoveryResponse, TenantInfo, UpcomingExam, WilmaProfile, StudentInfo, } from "./types.js";
2
2
  export { WilmaClient } from "./client.js";
3
3
  export { WilmaSession, AuthenticationError, APIError } from "./session.js";
4
4
  export { loadTenantDiscovery, listTenants, searchTenantsByMunicipality, findTenantByUrl, } from "./tenants.js";
@@ -1,9 +1,15 @@
1
1
  export function parseWilmaTimestamp(value) {
2
2
  if (value === null || value === undefined) {
3
- return new Date();
3
+ return fallbackDate();
4
4
  }
5
5
  if (typeof value === "number") {
6
- return new Date(value * 1000);
6
+ if (!Number.isFinite(value)) {
7
+ return fallbackDate();
8
+ }
9
+ // Wilma can provide Unix seconds or milliseconds.
10
+ // Values >= 1e12 are treated as milliseconds.
11
+ const millis = Math.abs(value) >= 1_000_000_000_000 ? value : value * 1000;
12
+ return new Date(millis);
7
13
  }
8
14
  const raw = String(value).trim();
9
15
  // Try ISO 8601 format first (e.g., "2026-02-05T13:56:42.737Z")
@@ -84,5 +90,10 @@ export function parseWilmaTimestamp(value) {
84
90
  }
85
91
  return candidate;
86
92
  }
87
- return now;
93
+ return fallbackDate();
94
+ }
95
+ function fallbackDate() {
96
+ // Keep unknown timestamps deterministic and old so recency filters
97
+ // do not treat undated items as fresh.
98
+ return new Date(0);
88
99
  }
@@ -0,0 +1,2 @@
1
+ import type { OverviewData } from "../types.js";
2
+ export declare function parseOverview(raw: unknown): OverviewData;
@@ -0,0 +1,152 @@
1
+ /* ------------------------------------------------------------------ */
2
+ /* Helpers */
3
+ /* ------------------------------------------------------------------ */
4
+ /** "4.9.2025" → "2025-09-04", passes through "2025-09-04" unchanged */
5
+ function parseFinnishDate(raw) {
6
+ const trimmed = raw.trim();
7
+ if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed))
8
+ return trimmed;
9
+ const m = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/.exec(trimmed);
10
+ if (m) {
11
+ return `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`;
12
+ }
13
+ return trimmed;
14
+ }
15
+ function firstTeacher(teachers) {
16
+ const t = teachers?.[0];
17
+ return { name: t?.TeacherName ?? "", code: t?.TeacherCode ?? "" };
18
+ }
19
+ function firstScheduleTeacher(teachers) {
20
+ const t = teachers?.[0];
21
+ return { name: t?.LongCaption ?? "", code: t?.Caption ?? "" };
22
+ }
23
+ /* ------------------------------------------------------------------ */
24
+ /* Main parser */
25
+ /* ------------------------------------------------------------------ */
26
+ export function parseOverview(raw) {
27
+ const data = (raw ?? {});
28
+ const now = new Date();
29
+ const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
30
+ return {
31
+ schedule: parseSchedule(data.Schedule ?? []),
32
+ upcomingExams: parseUpcomingExams(data.Groups ?? [], today),
33
+ grades: parseTopLevelGrades(data.Exams ?? []),
34
+ homework: parseHomework(data.Groups ?? []),
35
+ fetchedAt: now,
36
+ };
37
+ }
38
+ /* ------------------------------------------------------------------ */
39
+ /* Schedule */
40
+ /* ------------------------------------------------------------------ */
41
+ function parseSchedule(entries) {
42
+ const lessons = [];
43
+ for (const entry of entries) {
44
+ const dates = entry.DateArray ?? [];
45
+ const groups = entry.Groups ?? [];
46
+ const day = entry.Day ?? 0;
47
+ const start = entry.Start ?? "";
48
+ const end = entry.End ?? "";
49
+ for (const date of dates) {
50
+ for (const group of groups) {
51
+ const teacher = firstScheduleTeacher(group.Teachers);
52
+ lessons.push({
53
+ date,
54
+ dayOfWeek: day,
55
+ start,
56
+ end,
57
+ subject: group.FullCaption ?? group.Caption ?? "",
58
+ subjectCode: group.ShortCaption ?? "",
59
+ teacher: teacher.name,
60
+ teacherCode: teacher.code,
61
+ groupId: group.Id ?? 0,
62
+ });
63
+ }
64
+ }
65
+ }
66
+ lessons.sort((a, b) => a.date.localeCompare(b.date) || a.start.localeCompare(b.start));
67
+ return lessons;
68
+ }
69
+ /* ------------------------------------------------------------------ */
70
+ /* Exams & Grades (from Groups[].Exams[]) */
71
+ /* ------------------------------------------------------------------ */
72
+ /** Upcoming exams from Groups[].Exams[] — has ungraded exams with Caption/Topic */
73
+ function parseUpcomingExams(groups, today) {
74
+ const upcoming = [];
75
+ for (const group of groups) {
76
+ const teacher = firstTeacher(group.Teachers);
77
+ const subject = group.CourseName ?? "";
78
+ const subjectCode = group.CourseCode ?? "";
79
+ for (const exam of group.Exams ?? []) {
80
+ // Skip exams that already have a grade
81
+ if (exam.Grade != null && String(exam.Grade).trim() !== "")
82
+ continue;
83
+ const date = parseFinnishDate(exam.Date ?? "");
84
+ // Only include today or future
85
+ if (date < today)
86
+ continue;
87
+ upcoming.push({
88
+ examId: exam.Id ?? 0,
89
+ date,
90
+ name: exam.Caption ?? exam.Name ?? "",
91
+ subject,
92
+ subjectCode,
93
+ topic: exam.Topic?.trim() || null,
94
+ teacher: teacher.name,
95
+ teacherCode: teacher.code,
96
+ });
97
+ }
98
+ }
99
+ upcoming.sort((a, b) => a.date.localeCompare(b.date));
100
+ return upcoming;
101
+ }
102
+ /** Grades from top-level Exams[] — authoritative source for all graded exams */
103
+ function parseTopLevelGrades(exams) {
104
+ const grades = [];
105
+ for (const exam of exams) {
106
+ const grade = String(exam.Grade ?? "").trim();
107
+ if (!grade)
108
+ continue;
109
+ const date = parseFinnishDate(exam.Date ?? "");
110
+ const teacher = exam.Teachers?.[0];
111
+ grades.push({
112
+ examId: exam.ExamId ?? exam.Id ?? 0,
113
+ date,
114
+ name: exam.Name ?? "",
115
+ subject: exam.CourseTitle ?? "",
116
+ subjectCode: (exam.Course ?? "").split(" ")[0],
117
+ grade,
118
+ verbalGrade: null,
119
+ info: exam.Info?.trim() || null,
120
+ teacher: teacher?.TeacherName ?? "",
121
+ teacherCode: teacher?.TeacherCode ?? "",
122
+ });
123
+ }
124
+ grades.sort((a, b) => b.date.localeCompare(a.date));
125
+ return grades;
126
+ }
127
+ /* ------------------------------------------------------------------ */
128
+ /* Homework (from Groups[].Homework[]) */
129
+ /* ------------------------------------------------------------------ */
130
+ function parseHomework(groups) {
131
+ const items = [];
132
+ for (const group of groups) {
133
+ const teacher = firstTeacher(group.Teachers);
134
+ const subject = group.CourseName ?? "";
135
+ const subjectCode = group.CourseCode ?? "";
136
+ for (const hw of group.Homework ?? []) {
137
+ const text = (hw.Homework ?? "").replace(/\r\n/g, "\n").trim();
138
+ if (!text)
139
+ continue;
140
+ items.push({
141
+ date: hw.Date ?? "",
142
+ subject,
143
+ subjectCode,
144
+ homework: text,
145
+ teacher: teacher.name,
146
+ teacherCode: teacher.code,
147
+ });
148
+ }
149
+ }
150
+ items.sort((a, b) => b.date.localeCompare(a.date));
151
+ return items;
152
+ }
package/dist/types.d.ts CHANGED
@@ -57,3 +57,56 @@ export interface WilmaProfile {
57
57
  studentNumber?: string | null;
58
58
  debug?: boolean;
59
59
  }
60
+ export interface ScheduleLesson {
61
+ /** YYYY-MM-DD */
62
+ date: string;
63
+ /** 1=Monday ... 5=Friday */
64
+ dayOfWeek: number;
65
+ start: string;
66
+ end: string;
67
+ subject: string;
68
+ subjectCode: string;
69
+ teacher: string;
70
+ teacherCode: string;
71
+ groupId: number;
72
+ }
73
+ export interface UpcomingExam {
74
+ examId: number;
75
+ /** YYYY-MM-DD */
76
+ date: string;
77
+ name: string;
78
+ subject: string;
79
+ subjectCode: string;
80
+ topic: string | null;
81
+ teacher: string;
82
+ teacherCode: string;
83
+ }
84
+ export interface ExamGrade {
85
+ examId: number;
86
+ /** YYYY-MM-DD */
87
+ date: string;
88
+ name: string;
89
+ subject: string;
90
+ subjectCode: string;
91
+ grade: string;
92
+ verbalGrade: string | null;
93
+ info: string | null;
94
+ teacher: string;
95
+ teacherCode: string;
96
+ }
97
+ export interface HomeworkItem {
98
+ /** YYYY-MM-DD */
99
+ date: string;
100
+ subject: string;
101
+ subjectCode: string;
102
+ homework: string;
103
+ teacher: string;
104
+ teacherCode: string;
105
+ }
106
+ export interface OverviewData {
107
+ schedule: ScheduleLesson[];
108
+ upcomingExams: UpcomingExam[];
109
+ grades: ExamGrade[];
110
+ homework: HomeworkItem[];
111
+ fetchedAt: Date;
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wilm-ai/wilma-client",
3
- "version": "0.0.4",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",