iobroker.beszel 0.1.2
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/.github/auto-merge.yml +2 -0
- package/.github/dependabot.yml +12 -0
- package/.github/workflows/automerge-dependabot.yml +32 -0
- package/.github/workflows/test-and-release.yml +62 -0
- package/.vscode/settings.json +12 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +91 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/admin/beszel.svg +9 -0
- package/admin/i18n/de/translations.json +43 -0
- package/admin/i18n/en/translations.json +43 -0
- package/admin/i18n/es/translations.json +43 -0
- package/admin/i18n/fr/translations.json +43 -0
- package/admin/i18n/it/translations.json +43 -0
- package/admin/i18n/nl/translations.json +43 -0
- package/admin/i18n/pl/translations.json +43 -0
- package/admin/i18n/pt/translations.json +43 -0
- package/admin/i18n/ru/translations.json +43 -0
- package/admin/i18n/uk/translations.json +43 -0
- package/admin/i18n/zh-cn/translations.json +43 -0
- package/admin/jsonConfig.json +240 -0
- package/build/lib/beszel-client.d.ts +39 -0
- package/build/lib/beszel-client.d.ts.map +1 -0
- package/build/lib/beszel-client.js +199 -0
- package/build/lib/state-manager.d.ts +47 -0
- package/build/lib/state-manager.d.ts.map +1 -0
- package/build/lib/state-manager.js +738 -0
- package/build/lib/types.d.ts +174 -0
- package/build/lib/types.d.ts.map +1 -0
- package/build/lib/types.js +2 -0
- package/build/main.d.ts +2 -0
- package/build/main.d.ts.map +1 -0
- package/build/main.js +191 -0
- package/eslint.config.mjs +36 -0
- package/io-package.json +162 -0
- package/package.json +61 -0
- package/scripts/version.js +28 -0
- package/src/lib/beszel-client.ts +216 -0
- package/src/lib/state-manager.ts +1050 -0
- package/src/lib/types.ts +192 -0
- package/src/main.ts +199 -0
- package/test/testPackageFiles.ts +5 -0
- package/tsconfig.build.json +7 -0
- package/tsconfig.json +24 -0
- package/tsconfig.test.json +9 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import * as http from "http";
|
|
2
|
+
import * as https from "https";
|
|
3
|
+
import { URL } from "url";
|
|
4
|
+
import type {
|
|
5
|
+
AuthResponse,
|
|
6
|
+
BeszelContainer,
|
|
7
|
+
BeszelSystem,
|
|
8
|
+
BeszelSystemStats,
|
|
9
|
+
PocketBaseList,
|
|
10
|
+
SystemStats,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
|
|
13
|
+
const TOKEN_REFRESH_MS = 23 * 60 * 60 * 1000; // 23 hours
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* HTTP client for the Beszel PocketBase REST API.
|
|
17
|
+
* Uses only Node.js built-in http/https — no extra dependencies.
|
|
18
|
+
*/
|
|
19
|
+
export class BeszelClient {
|
|
20
|
+
private readonly baseUrl: string;
|
|
21
|
+
private readonly username: string;
|
|
22
|
+
private readonly password: string;
|
|
23
|
+
|
|
24
|
+
private token: string | null = null;
|
|
25
|
+
private tokenTime = 0;
|
|
26
|
+
|
|
27
|
+
constructor(url: string, username: string, password: string) {
|
|
28
|
+
// Strip trailing slash
|
|
29
|
+
this.baseUrl = url.replace(/\/+$/, "");
|
|
30
|
+
this.username = username;
|
|
31
|
+
this.password = password;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Force token re-authentication on the next request */
|
|
35
|
+
public invalidateToken(): void {
|
|
36
|
+
this.token = null;
|
|
37
|
+
this.tokenTime = 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Test the connection to Beszel.
|
|
42
|
+
* Returns { success: true } or { success: false, message: reason }.
|
|
43
|
+
*/
|
|
44
|
+
public async checkConnection(): Promise<{
|
|
45
|
+
success: boolean;
|
|
46
|
+
message: string;
|
|
47
|
+
}> {
|
|
48
|
+
try {
|
|
49
|
+
this.invalidateToken();
|
|
50
|
+
await this.authenticate();
|
|
51
|
+
return { success: true, message: "Connected successfully" };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
message: err instanceof Error ? err.message : String(err),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Fetch all systems */
|
|
61
|
+
public async getSystems(): Promise<BeszelSystem[]> {
|
|
62
|
+
await this.ensureToken();
|
|
63
|
+
const data = await this.fetchJson<PocketBaseList<BeszelSystem>>(
|
|
64
|
+
"/api/collections/systems/records?perPage=200&sort=name",
|
|
65
|
+
);
|
|
66
|
+
return data.items;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fetch the latest 1m stats per system.
|
|
71
|
+
* Returns a Map<systemId, SystemStats>.
|
|
72
|
+
*
|
|
73
|
+
* @param systemIds
|
|
74
|
+
*/
|
|
75
|
+
public async getLatestStats(
|
|
76
|
+
systemIds: string[],
|
|
77
|
+
): Promise<Map<string, SystemStats>> {
|
|
78
|
+
if (systemIds.length === 0) {
|
|
79
|
+
return new Map();
|
|
80
|
+
}
|
|
81
|
+
await this.ensureToken();
|
|
82
|
+
const data = await this.fetchJson<PocketBaseList<BeszelSystemStats>>(
|
|
83
|
+
"/api/collections/system_stats/records?sort=-updated&perPage=200&filter=type%3D'1m'",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Deduplicate: keep the newest record per system
|
|
87
|
+
const result = new Map<string, SystemStats>();
|
|
88
|
+
for (const record of data.items) {
|
|
89
|
+
if (!result.has(record.system)) {
|
|
90
|
+
result.set(record.system, record.stats);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Fetch all containers */
|
|
97
|
+
public async getContainers(): Promise<BeszelContainer[]> {
|
|
98
|
+
await this.ensureToken();
|
|
99
|
+
const data = await this.fetchJson<PocketBaseList<BeszelContainer>>(
|
|
100
|
+
"/api/collections/containers/records?perPage=500&sort=system%2Cname",
|
|
101
|
+
);
|
|
102
|
+
return data.items;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// -------------------------------------------------------------------------
|
|
106
|
+
// Private helpers
|
|
107
|
+
// -------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
private async ensureToken(): Promise<void> {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
if (this.token && now - this.tokenTime < TOKEN_REFRESH_MS) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await this.authenticate();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async authenticate(): Promise<void> {
|
|
118
|
+
const body = JSON.stringify({
|
|
119
|
+
identity: this.username,
|
|
120
|
+
password: this.password,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const data = await this.request<AuthResponse>(
|
|
124
|
+
"POST",
|
|
125
|
+
"/api/collections/users/auth-with-password",
|
|
126
|
+
body,
|
|
127
|
+
null, // no auth token yet
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
this.token = data.token;
|
|
131
|
+
this.tokenTime = Date.now();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async fetchJson<T>(path: string): Promise<T> {
|
|
135
|
+
return this.request<T>("GET", path, null, this.token);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private request<T>(
|
|
139
|
+
method: string,
|
|
140
|
+
path: string,
|
|
141
|
+
body: string | null,
|
|
142
|
+
token: string | null,
|
|
143
|
+
): Promise<T> {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
let parsedUrl: URL;
|
|
146
|
+
try {
|
|
147
|
+
parsedUrl = new URL(this.baseUrl + path);
|
|
148
|
+
} catch {
|
|
149
|
+
reject(new Error(`Invalid URL: ${this.baseUrl + path}`));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const isHttps = parsedUrl.protocol === "https:";
|
|
154
|
+
const transport = isHttps ? https : http;
|
|
155
|
+
|
|
156
|
+
const headers: Record<string, string> = {
|
|
157
|
+
"Content-Type": "application/json",
|
|
158
|
+
Accept: "application/json",
|
|
159
|
+
};
|
|
160
|
+
if (token) {
|
|
161
|
+
headers.Authorization = token;
|
|
162
|
+
}
|
|
163
|
+
if (body !== null) {
|
|
164
|
+
headers["Content-Length"] = Buffer.byteLength(body).toString();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const options: http.RequestOptions = {
|
|
168
|
+
hostname: parsedUrl.hostname,
|
|
169
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
170
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
171
|
+
method,
|
|
172
|
+
headers,
|
|
173
|
+
timeout: 15000,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const req = transport.request(options, (res) => {
|
|
177
|
+
const chunks: Buffer[] = [];
|
|
178
|
+
res.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
179
|
+
res.on("end", () => {
|
|
180
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
181
|
+
if (
|
|
182
|
+
!res.statusCode ||
|
|
183
|
+
res.statusCode < 200 ||
|
|
184
|
+
res.statusCode >= 300
|
|
185
|
+
) {
|
|
186
|
+
// Propagate 401 specifically so caller can re-auth
|
|
187
|
+
const err = new Error(
|
|
188
|
+
`HTTP ${res.statusCode ?? "?"}: ${raw.slice(0, 200)}`,
|
|
189
|
+
);
|
|
190
|
+
(err as NodeJS.ErrnoException).code =
|
|
191
|
+
res.statusCode === 401 ? "UNAUTHORIZED" : "HTTP_ERROR";
|
|
192
|
+
reject(err);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
resolve(JSON.parse(raw) as T);
|
|
197
|
+
} catch {
|
|
198
|
+
reject(new Error(`Invalid JSON response from ${path}`));
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
req.on("timeout", () => {
|
|
204
|
+
req.destroy();
|
|
205
|
+
reject(new Error(`Request to ${path} timed out`));
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
req.on("error", (err) => reject(err));
|
|
209
|
+
|
|
210
|
+
if (body !== null) {
|
|
211
|
+
req.write(body);
|
|
212
|
+
}
|
|
213
|
+
req.end();
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|