feat!: release v2 (#6545)

Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
This commit is contained in:
3286 files changed
+284386 -139176

No files matched your search

@@ -35,7 +35,11 @@ export class GlancesIntegration extends Integration implements ISystemHealthMoni
if (session == null) throw new Error("Session was unexpectitly null");
const [stats, cpuTemp] = await Promise.all([this.getAllStatsAsync(), this.getCpuTempAsync()]);
const [stats, cpuTemp, smart] = await Promise.all([
this.getAllStatsAsync(),
this.getCpuTempAsync(),
this.getSmartAsync(),
]);
return {
cpuUtilization: stats.cpu.total,
@@ -57,7 +61,7 @@ export class GlancesIntegration extends Integration implements ISystemHealthMoni
rebootRequired: false,
cpuModelName: stats.quicklook?.cpu_name ?? "Unknown",
loadAverage: null,
smart: [],
smart,
cpuTemp,
gpu: stats.gpu.map((gpu) => ({
gpuId: gpu.gpu_id,
@@ -97,6 +101,23 @@ export class GlancesIntegration extends Integration implements ISystemHealthMoni
}
}
private async getSmartAsync(): Promise<SystemHealthMonitoring["smart"]> {
// The SMART plugin is disabled by default (Glances answers 400), so a missing
// or unexpected /api/4/smart response must not fail getSystemInfoAsync().
try {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/4/smart"));
if (!response.ok) {
return [];
}
const devices = await smartSchema.parseAsync(await response.json());
return devices.flatMap(mapGlancesSmartDevice);
} catch {
return [];
}
}
private async getAllStatsAsync() {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/4/all"));
@@ -220,3 +241,74 @@ export const parseGlancesCpuTempFromSensors = (sensors: z.infer<typeof sensorsSc
return undefined;
};
// Glances (pySMART) returns one object per disk: "DeviceName" is "<device> <model>",
// every other object value is a SMART attribute (ATA) or an NVMe health log entry.
const smartAttributeSchema = z.object({
name: z.string().nullable().optional(),
key: z.string().nullable().optional(),
value: z.union([z.string(), z.number()]).nullable().optional(),
raw: z.union([z.string(), z.number()]).nullable().optional(),
when_failed: z.string().nullable().optional(),
});
const smartSchema = z.array(z.object({ DeviceName: z.string() }).catchall(z.unknown()));
const ataTemperatureNames = ["Temperature_Celsius", "Airflow_Temperature_Cel"] as const;
const mapGlancesSmartDevice = (device: z.infer<typeof smartSchema>[number]): SystemHealthMonitoring["smart"] => {
const attributes = Object.values(device).flatMap((value) => {
const result = smartAttributeSchema.safeParse(value);
return result.success ? [result.data] : [];
});
// ATA: when_failed is "-" unless the attribute crossed its threshold. NVMe: critical warning bitmask.
const failedAttributes = attributes
.filter((attribute) => attribute.when_failed && attribute.when_failed !== "-")
.flatMap((attribute) => attribute.name ?? attribute.key ?? []);
const criticalWarningValue = attributes.find((attribute) => attribute.key === "criticalWarning")?.value;
const criticalWarningCount = criticalWarningValue == null ? Number.NaN : Number(criticalWarningValue);
const isNvme = Number.isFinite(criticalWarningCount);
// Glances lists a device even when no health attribute was read (or all are hidden by hide_attributes).
// Without ATA thresholds or an NVMe critical warning there is no verdict, so the widget shows N/A.
const hasAtaThresholds = attributes.some((attribute) => typeof attribute.when_failed === "string");
if (!hasAtaThresholds && !isNvme) return [];
const hasCriticalWarning = isNvme && criticalWarningCount !== 0;
const healthy = failedAttributes.length === 0 && !hasCriticalWarning;
return [
{
deviceName: `/dev/${device.DeviceName.split(" ")[0]}`,
temperature: getSmartTemperature(attributes),
overallStatus: healthy ? "PASSED" : "FAILED",
healthy,
statusReason: getSmartStatusReason(failedAttributes, isNvme, hasCriticalWarning),
},
];
};
const getSmartStatusReason = (
failedAttributes: string[],
isNvme: boolean,
hasCriticalWarning: boolean,
): SystemHealthMonitoring["smart"][number]["statusReason"] => {
if (failedAttributes.length > 0) return { type: "attributesFailed", attributes: failedAttributes };
if (hasCriticalWarning) return { type: "criticalWarning" };
return { type: isNvme ? "noCriticalWarning" : "attributesWithinThresholds" };
};
const getSmartTemperature = (attributes: z.infer<typeof smartAttributeSchema>[]): number | null => {
const nvmeTemperature = attributes.find((attribute) => attribute.key === "_temperature")?.value;
if (typeof nvmeTemperature === "number") return nvmeTemperature;
for (const name of ataTemperatureNames) {
// ATA raw values may carry extra data, e.g. "35 (Min/Max 20/45)".
const raw = attributes.find((attribute) => attribute.name === name)?.raw;
const temperature = Number.parseInt(String(raw ?? ""), 10);
if (!Number.isNaN(temperature)) return temperature;
}
return null;
};