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.
Files changed (46) hide show
  1. package/.github/auto-merge.yml +2 -0
  2. package/.github/dependabot.yml +12 -0
  3. package/.github/workflows/automerge-dependabot.yml +32 -0
  4. package/.github/workflows/test-and-release.yml +62 -0
  5. package/.vscode/settings.json +12 -0
  6. package/CHANGELOG.md +13 -0
  7. package/CLAUDE.md +91 -0
  8. package/LICENSE +21 -0
  9. package/README.md +187 -0
  10. package/admin/beszel.svg +9 -0
  11. package/admin/i18n/de/translations.json +43 -0
  12. package/admin/i18n/en/translations.json +43 -0
  13. package/admin/i18n/es/translations.json +43 -0
  14. package/admin/i18n/fr/translations.json +43 -0
  15. package/admin/i18n/it/translations.json +43 -0
  16. package/admin/i18n/nl/translations.json +43 -0
  17. package/admin/i18n/pl/translations.json +43 -0
  18. package/admin/i18n/pt/translations.json +43 -0
  19. package/admin/i18n/ru/translations.json +43 -0
  20. package/admin/i18n/uk/translations.json +43 -0
  21. package/admin/i18n/zh-cn/translations.json +43 -0
  22. package/admin/jsonConfig.json +240 -0
  23. package/build/lib/beszel-client.d.ts +39 -0
  24. package/build/lib/beszel-client.d.ts.map +1 -0
  25. package/build/lib/beszel-client.js +199 -0
  26. package/build/lib/state-manager.d.ts +47 -0
  27. package/build/lib/state-manager.d.ts.map +1 -0
  28. package/build/lib/state-manager.js +738 -0
  29. package/build/lib/types.d.ts +174 -0
  30. package/build/lib/types.d.ts.map +1 -0
  31. package/build/lib/types.js +2 -0
  32. package/build/main.d.ts +2 -0
  33. package/build/main.d.ts.map +1 -0
  34. package/build/main.js +191 -0
  35. package/eslint.config.mjs +36 -0
  36. package/io-package.json +162 -0
  37. package/package.json +61 -0
  38. package/scripts/version.js +28 -0
  39. package/src/lib/beszel-client.ts +216 -0
  40. package/src/lib/state-manager.ts +1050 -0
  41. package/src/lib/types.ts +192 -0
  42. package/src/main.ts +199 -0
  43. package/test/testPackageFiles.ts +5 -0
  44. package/tsconfig.build.json +7 -0
  45. package/tsconfig.json +24 -0
  46. package/tsconfig.test.json +9 -0
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BeszelClient = void 0;
37
+ const http = __importStar(require("http"));
38
+ const https = __importStar(require("https"));
39
+ const url_1 = require("url");
40
+ const TOKEN_REFRESH_MS = 23 * 60 * 60 * 1000; // 23 hours
41
+ /**
42
+ * HTTP client for the Beszel PocketBase REST API.
43
+ * Uses only Node.js built-in http/https — no extra dependencies.
44
+ */
45
+ class BeszelClient {
46
+ baseUrl;
47
+ username;
48
+ password;
49
+ token = null;
50
+ tokenTime = 0;
51
+ constructor(url, username, password) {
52
+ // Strip trailing slash
53
+ this.baseUrl = url.replace(/\/+$/, "");
54
+ this.username = username;
55
+ this.password = password;
56
+ }
57
+ /** Force token re-authentication on the next request */
58
+ invalidateToken() {
59
+ this.token = null;
60
+ this.tokenTime = 0;
61
+ }
62
+ /**
63
+ * Test the connection to Beszel.
64
+ * Returns { success: true } or { success: false, message: reason }.
65
+ */
66
+ async checkConnection() {
67
+ try {
68
+ this.invalidateToken();
69
+ await this.authenticate();
70
+ return { success: true, message: "Connected successfully" };
71
+ }
72
+ catch (err) {
73
+ return {
74
+ success: false,
75
+ message: err instanceof Error ? err.message : String(err),
76
+ };
77
+ }
78
+ }
79
+ /** Fetch all systems */
80
+ async getSystems() {
81
+ await this.ensureToken();
82
+ const data = await this.fetchJson("/api/collections/systems/records?perPage=200&sort=name");
83
+ return data.items;
84
+ }
85
+ /**
86
+ * Fetch the latest 1m stats per system.
87
+ * Returns a Map<systemId, SystemStats>.
88
+ *
89
+ * @param systemIds
90
+ */
91
+ async getLatestStats(systemIds) {
92
+ if (systemIds.length === 0) {
93
+ return new Map();
94
+ }
95
+ await this.ensureToken();
96
+ const data = await this.fetchJson("/api/collections/system_stats/records?sort=-updated&perPage=200&filter=type%3D'1m'");
97
+ // Deduplicate: keep the newest record per system
98
+ const result = new Map();
99
+ for (const record of data.items) {
100
+ if (!result.has(record.system)) {
101
+ result.set(record.system, record.stats);
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+ /** Fetch all containers */
107
+ async getContainers() {
108
+ await this.ensureToken();
109
+ const data = await this.fetchJson("/api/collections/containers/records?perPage=500&sort=system%2Cname");
110
+ return data.items;
111
+ }
112
+ // -------------------------------------------------------------------------
113
+ // Private helpers
114
+ // -------------------------------------------------------------------------
115
+ async ensureToken() {
116
+ const now = Date.now();
117
+ if (this.token && now - this.tokenTime < TOKEN_REFRESH_MS) {
118
+ return;
119
+ }
120
+ await this.authenticate();
121
+ }
122
+ async authenticate() {
123
+ const body = JSON.stringify({
124
+ identity: this.username,
125
+ password: this.password,
126
+ });
127
+ const data = await this.request("POST", "/api/collections/users/auth-with-password", body, null);
128
+ this.token = data.token;
129
+ this.tokenTime = Date.now();
130
+ }
131
+ async fetchJson(path) {
132
+ return this.request("GET", path, null, this.token);
133
+ }
134
+ request(method, path, body, token) {
135
+ return new Promise((resolve, reject) => {
136
+ let parsedUrl;
137
+ try {
138
+ parsedUrl = new url_1.URL(this.baseUrl + path);
139
+ }
140
+ catch {
141
+ reject(new Error(`Invalid URL: ${this.baseUrl + path}`));
142
+ return;
143
+ }
144
+ const isHttps = parsedUrl.protocol === "https:";
145
+ const transport = isHttps ? https : http;
146
+ const headers = {
147
+ "Content-Type": "application/json",
148
+ Accept: "application/json",
149
+ };
150
+ if (token) {
151
+ headers.Authorization = token;
152
+ }
153
+ if (body !== null) {
154
+ headers["Content-Length"] = Buffer.byteLength(body).toString();
155
+ }
156
+ const options = {
157
+ hostname: parsedUrl.hostname,
158
+ port: parsedUrl.port || (isHttps ? 443 : 80),
159
+ path: parsedUrl.pathname + parsedUrl.search,
160
+ method,
161
+ headers,
162
+ timeout: 15000,
163
+ };
164
+ const req = transport.request(options, (res) => {
165
+ const chunks = [];
166
+ res.on("data", (chunk) => chunks.push(chunk));
167
+ res.on("end", () => {
168
+ const raw = Buffer.concat(chunks).toString("utf8");
169
+ if (!res.statusCode ||
170
+ res.statusCode < 200 ||
171
+ res.statusCode >= 300) {
172
+ // Propagate 401 specifically so caller can re-auth
173
+ const err = new Error(`HTTP ${res.statusCode ?? "?"}: ${raw.slice(0, 200)}`);
174
+ err.code =
175
+ res.statusCode === 401 ? "UNAUTHORIZED" : "HTTP_ERROR";
176
+ reject(err);
177
+ return;
178
+ }
179
+ try {
180
+ resolve(JSON.parse(raw));
181
+ }
182
+ catch {
183
+ reject(new Error(`Invalid JSON response from ${path}`));
184
+ }
185
+ });
186
+ });
187
+ req.on("timeout", () => {
188
+ req.destroy();
189
+ reject(new Error(`Request to ${path} timed out`));
190
+ });
191
+ req.on("error", (err) => reject(err));
192
+ if (body !== null) {
193
+ req.write(body);
194
+ }
195
+ req.end();
196
+ });
197
+ }
198
+ }
199
+ exports.BeszelClient = BeszelClient;
@@ -0,0 +1,47 @@
1
+ import type * as utils from "@iobroker/adapter-core";
2
+ import type { AdapterConfig, BeszelContainer, BeszelSystem, SystemStats } from "./types.js";
3
+ /**
4
+ * Manages creation and updates of ioBroker states for Beszel systems.
5
+ */
6
+ export declare class StateManager {
7
+ private readonly adapter;
8
+ constructor(adapter: utils.AdapterInstance);
9
+ /**
10
+ * Sanitize a name to a valid ioBroker state ID segment.
11
+ * Lowercase, replace non-alphanumeric with _, max 50 chars, trim underscores.
12
+ *
13
+ * @param name
14
+ */
15
+ sanitize(name: string): string;
16
+ /**
17
+ * Update all states for a single system.
18
+ *
19
+ * @param system
20
+ * @param stats
21
+ * @param containers
22
+ * @param config
23
+ */
24
+ updateSystem(system: BeszelSystem, stats: SystemStats | undefined, containers: BeszelContainer[], config: AdapterConfig): Promise<void>;
25
+ /**
26
+ * Remove device objects for systems that are no longer in Beszel.
27
+ *
28
+ * @param activeSystemNames
29
+ */
30
+ cleanupSystems(activeSystemNames: string[]): Promise<void>;
31
+ /**
32
+ * Delete states for metrics that have been disabled in the config.
33
+ * Called on startup to clean up previously-enabled states.
34
+ *
35
+ * @param systemId
36
+ * @param config
37
+ */
38
+ cleanupMetrics(systemId: string, config: AdapterConfig): Promise<void>;
39
+ private updateStatsStates;
40
+ private updateContainers;
41
+ private ensureChannel;
42
+ private deleteChannelIfExists;
43
+ private createAndSetState;
44
+ private computeTopAvgTemp;
45
+ private formatUptime;
46
+ }
47
+ //# sourceMappingURL=state-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-manager.d.ts","sourceRoot":"","sources":["../../src/lib/state-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EACV,aAAa,EACb,eAAe,EACf,YAAY,EACZ,WAAW,EACZ,MAAM,YAAY,CAAC;AAKpB;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;gBAEpC,OAAO,EAAE,KAAK,CAAC,eAAe;IAI1C;;;;;OAKG;IACI,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAQrC;;;;;;;OAOG;IACU,YAAY,CACvB,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,UAAU,EAAE,eAAe,EAAE,EAC7B,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,IAAI,CAAC;IAgKhB;;;;OAIG;IACU,cAAc,CAAC,iBAAiB,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAkCvE;;;;;;OAMG;IACU,cAAc,CACzB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,IAAI,CAAC;YA2FF,iBAAiB;YAsiBjB,gBAAgB;YAqFhB,aAAa;YAQb,qBAAqB;YAWrB,iBAAiB;IAa/B,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,YAAY;CAgBrB"}