Files
community-plugins/drive-health/panel.luau
T

1686 lines
68 KiB
Luau

--!nonstrict
local snapshot = noctalia.state.get("snapshot")
local history = noctalia.state.get("drive_history") or { drives = {} }
local preferences = noctalia.state.get("drive_preferences") or { schema = 1, order = {}, drives = {} }
local opened = false
local expandedDriveId = nil
local editingDriveId = nil
local currentDrives = {}
local currentIssues = {}
local dismissRequestNonce = 0
local pendingSelfTest = nil
local selfTestLaunch = nil
local intervalApply = nil
local privilegedAction = nil
local confirmUninstall = false
local showCollectorSettings = false
local hiddenSelection = 1
local aliasDraft = ""
local warningDraft = ""
local criticalDraft = ""
local lifeDraft = ""
local alertsDraft = true
local presenceDraft = false
local EDITABLE_PREFERENCE_FIELDS = {
"alias", "warning_temperature", "critical_temperature", "life_warning_percent",
"alerts_enabled", "presence_alert_enabled",
}
local MIN_TREND_SAMPLES = 4
local function number(value, fallback)
local parsed = tonumber(value)
return parsed ~= nil and parsed or fallback
end
local function clamp(value, minimum, maximum)
return math.max(minimum, math.min(maximum, value))
end
local function formatBytes(value)
local bytes = tonumber(value)
if bytes == nil then
return noctalia.tr("common.not_available")
end
local units = { "B", "KiB", "MiB", "GiB", "TiB", "PiB" }
local index = 1
while bytes >= 1024 and index < #units do
bytes /= 1024
index += 1
end
if index <= 3 then
return string.format("%.0f %s", bytes, units[index])
end
return string.format("%.1f %s", bytes, units[index])
end
local function formatHours(value)
local hours = tonumber(value)
if hours == nil then
return noctalia.tr("common.not_available")
end
if hours >= 8760 then
return noctalia.tr("common.duration_years", { value = string.format("%.1f", hours / 8760) })
elseif hours >= 24 then
return noctalia.tr("common.duration_days", { value = string.format("%.0f", hours / 24) })
end
return noctalia.tr("common.duration_hours", { value = string.format("%.0f", hours) })
end
local function formatPowerOn(drive)
local formatted = formatHours(drive.power_on_hours)
return drive.power_on_hours_saturated and "≥ " .. formatted or formatted
end
local function formatPercent(value)
local percent = tonumber(value)
return percent ~= nil and string.format("%.0f%%", percent) or noctalia.tr("common.not_available")
end
local function shellQuote(value)
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
end
local function driveId(drive)
return tostring(drive.id or drive.serial or drive.device or "unknown")
end
local function driveName(drive)
return tostring(drive.display_name or drive.model or drive.device or noctalia.tr("alerts.unknown_drive"))
end
local function preferenceFor(id)
preferences.drives = type(preferences.drives) == "table" and preferences.drives or {}
local entry = preferences.drives[id]
if type(entry) ~= "table" then
entry = {}
preferences.drives[id] = entry
end
return entry
end
local function savePreferences()
local directory = noctalia.pluginDataDir()
if directory == nil then
noctalia.notifyError(noctalia.tr("preferences.title"), noctalia.tr("preferences.save_failed"))
return false
end
preferences.schema = 1
preferences.order = type(preferences.order) == "table" and preferences.order or {}
preferences.drives = type(preferences.drives) == "table" and preferences.drives or {}
local encoded = noctalia.json.encode(preferences, true)
if encoded == nil then
noctalia.notifyError(noctalia.tr("preferences.title"), noctalia.tr("preferences.save_failed"))
return false
end
local path = directory .. "/drive-preferences.json"
local temporary = path .. ".tmp"
if not noctalia.writeFile(temporary, encoded) or not noctalia.renameFile(temporary, path) then
noctalia.notifyError(noctalia.tr("preferences.title"), noctalia.tr("preferences.save_failed"))
return false
end
noctalia.state.set("drive_preferences", preferences)
return true
end
local function temperatureColor(value, drive)
local warning = number(drive and drive.warning_temperature,
number(noctalia.getConfig("warning_temperature"), 65))
local critical = math.max(warning + 1, number(drive and drive.critical_temperature,
number(noctalia.getConfig("critical_temperature"), 80)))
if value == nil then
return "on_surface_variant"
elseif value >= critical then
return "error"
elseif value >= warning then
return "secondary"
end
return "primary"
end
local function metric(icon, label, value, color)
return ui.row({ gap = 8, align = "center", flexGrow = 1 }, {
ui.glyph({ name = icon, size = 14, color = color or "on_surface_variant" }),
ui.column({ gap = 1, flexGrow = 1 }, {
ui.label({ text = label, fontSize = 11, color = "on_surface_variant" }),
ui.label({ text = value, fontWeight = "medium", color = color or "on_surface" }),
}),
})
end
local function progressMetric(label, value, display, fill)
local progress = value ~= nil and clamp(number(value, 0) / 100, 0, 1) or 0
return ui.column({ gap = 5, flexGrow = 1, align = "stretch" }, {
ui.row({ justify = "space_between", align = "center" }, {
ui.label({ text = label, fontSize = 11, color = "on_surface_variant" }),
ui.label({ text = display, fontSize = 11, fontWeight = "bold", color = fill }),
}),
ui.progress({ progress = progress, fill = fill, height = 6, radius = 3 }),
})
end
local function summaryCard(value, label, detail, color)
local children = {
ui.label({ text = value, fontSize = 20, fontWeight = "bold", color = color }),
ui.label({ text = label, fontSize = 11, color = "on_surface_variant" }),
}
if detail ~= nil and tostring(detail) ~= "" then
table.insert(children, ui.label({ text = tostring(detail), fontSize = 9,
color = "on_surface_variant/0.78", maxLines = 1 }))
end
return ui.column({ gap = 2, padding = 10, radius = 10,
fill = color .. "/0.12", flexGrow = 1 }, children)
end
local function healthInfo(drive)
if drive.health == "passed" then
return noctalia.tr("health.passed"), "primary", "check"
elseif drive.health == "failed" then
return noctalia.tr("health.failed"), "error", "alert-triangle"
end
return noctalia.tr("health.unknown"), "on_surface_variant", "help-circle"
end
local function appendMetricRows(target, metrics)
local index = 1
while index <= #metrics do
local row = { metrics[index] }
if metrics[index + 1] ~= nil then
table.insert(row, metrics[index + 1])
end
table.insert(target, ui.row({ gap = 14, align = "center" }, row))
index += 2
end
end
local function alertsCard(issues)
if type(issues) ~= "table" or #issues == 0 then
return nil
end
local header = {
ui.glyph({ name = "alert-triangle", size = 17, color = "error" }),
ui.label({
text = noctalia.tr("alerts.active_title", { count = #issues }),
fontWeight = "bold",
color = "error",
flexGrow = 1,
}),
}
table.insert(header, ui.button({ text = noctalia.tr("alerts.dismiss_all"), variant = "ghost", controlSize = "sm",
onClick = "onDismissAllAlertsClicked" }))
local rows = { ui.row({ gap = 7, align = "center" }, header) }
for index, issue in ipairs(issues) do
local color = issue.severity == "critical" and "error" or "secondary"
table.insert(rows, ui.row({ gap = 8, align = "center" }, {
ui.glyph({ name = issue.severity == "critical" and "alert-octagon" or "alert-circle", size = 13, color = color }),
ui.label({ text = tostring(issue.message or issue.title), fontSize = 11, color = "on_surface", flexGrow = 1 }),
ui.button({ glyph = "x", tooltip = noctalia.tr("alerts.dismiss"), variant = "ghost", controlSize = "sm",
enabled = index <= 32, onClick = "onDismissAlert" .. tostring(index) .. "Clicked" }),
}))
end
return ui.column({ gap = 7, padding = 11, radius = 10, fill = "error/0.10", border = "error/0.30", borderWidth = 1 }, rows)
end
local function dependencyCard(dependencies)
if type(dependencies) ~= "table" or dependencies.ready then
return nil
end
local children = {
ui.row({ gap = 8, align = "center" }, {
ui.glyph({ name = "package", size = 18, color = "error" }),
ui.column({ gap = 2, flexGrow = 1 }, {
ui.label({ text = noctalia.tr("dependencies.title"), fontWeight = "bold", color = "error" }),
ui.label({
text = noctalia.tr("dependencies.missing", { missing = tostring(dependencies.missing_text or "") }),
fontSize = 11,
color = "on_surface_variant",
flexGrow = 1,
}),
}),
}),
}
if dependencies.install_command ~= nil and dependencies.install_command ~= "" then
table.insert(children, ui.column({ gap = 4, fill = "surface/0.48", radius = 8, padding = 8 }, {
ui.label({
text = noctalia.tr("dependencies.command", {
manager = tostring(dependencies.package_manager or noctalia.tr("dependencies.package_manager")),
}),
fontSize = 10,
color = "on_surface_variant",
}),
ui.label({ text = tostring(dependencies.install_command), fontSize = 10, color = "on_surface", maxLines = 2 }),
}))
else
table.insert(children, ui.label({
text = noctalia.tr("dependencies.manual_install"),
fontSize = 11,
color = "on_surface_variant",
}))
end
if privilegedAction ~= nil and privilegedAction.scope == "dependencies" then
table.insert(children, ui.label({
text = privilegedAction.state == "authorizing"
and noctalia.tr("privileged_action.authorizing", { action = noctalia.tr("dependencies.action_install") })
or noctalia.tr("privileged_action.failed", { action = noctalia.tr("dependencies.action_install"),
error = tostring(privilegedAction.error or "") }),
fontSize = 10,
color = privilegedAction.state == "authorizing" and "secondary" or "error",
maxLines = 2,
}))
end
local actions = {}
if dependencies.can_install and dependencies.install_command ~= nil then
table.insert(actions, ui.button({
text = noctalia.tr("dependencies.install"),
variant = "primary",
onClick = "onInstallDependenciesClicked",
}))
table.insert(actions, ui.button({
text = noctalia.tr("dependencies.copy_command"),
variant = "outline",
onClick = "onCopyInstallCommandClicked",
}))
end
table.insert(actions, ui.button({
text = noctalia.tr("dependencies.recheck"),
variant = "ghost",
onClick = "onRefreshClicked",
}))
table.insert(children, ui.row({ gap = 8, align = "center" }, actions))
return ui.column({
gap = 9,
padding = 11,
radius = 10,
fill = "error/0.10",
border = "error/0.32",
borderWidth = 1,
}, children)
end
local function collectorCard(collector)
if showCollectorSettings or type(collector) ~= "table" then
return nil
end
local status = tostring(collector.status or "not-installed")
if status == "healthy" or status == "disabled" then
return nil
end
local color = status == "stale" and "error" or "secondary"
local actions = {}
local actionKey = status == "stale" and "collector.start"
or (collector.installed and "collector.upgrade" or "collector.install")
local actionHandler = status == "stale" and "onStartCollectorClicked"
or "onInstallCollectorClicked"
table.insert(actions, ui.button({
text = noctalia.tr(actionKey), variant = "primary", controlSize = "sm", onClick = actionHandler,
}))
table.insert(actions, ui.button({
text = noctalia.tr("collector.copy_install"), variant = "outline", controlSize = "sm",
onClick = "onCopyCollectorCommandClicked",
}))
if collector.installed then
table.insert(actions, ui.button({
text = noctalia.tr("collector.remove"), variant = confirmUninstall and "destructive" or "ghost",
controlSize = "sm", onClick = "onUninstallCollectorClicked",
}))
end
table.insert(actions, ui.button({
text = noctalia.tr("dependencies.recheck"), variant = "ghost", controlSize = "sm",
onClick = "onRefreshClicked",
}))
local children = {
ui.row({ gap = 8, align = "center" }, {
ui.glyph({ name = "shield-cog", size = 17, color = color }),
ui.column({ gap = 1, flexGrow = 1 }, {
ui.label({ text = noctalia.tr("collector.title"), fontWeight = "bold", color = color }),
ui.label({
text = noctalia.tr("collector.status_" .. status:gsub("%-", "_"), {
version = tostring(collector.version or collector.expected_version or ""),
}),
fontSize = 11, color = "on_surface_variant",
}),
}),
}),
}
if confirmUninstall then
table.insert(children, ui.label({
text = noctalia.tr("collector.remove_confirm"), fontSize = 11, color = "error",
}))
end
table.insert(children, ui.row({ gap = 7, align = "center" }, actions))
return ui.column({
gap = 8, padding = 10, radius = 10, fill = color .. "/0.08",
border = color .. "/0.24", borderWidth = 1,
}, children)
end
local function collectorSettingsCard(collector)
if not showCollectorSettings or type(collector) ~= "table" then
return nil
end
local status = tostring(collector.status or "not-installed")
local enabled = collector.enabled == true
local color = status == "healthy" and "primary"
or (status == "disabled" and "on_surface_variant"
or (status == "stale" and "error" or "secondary"))
local actions = {
ui.button({ text = noctalia.tr("collector.open_settings"), glyph = "settings", variant = "outline",
controlSize = "sm", onClick = "onOpenPluginSettingsClicked" }),
}
if enabled and status == "not-installed" then
table.insert(actions, 1, ui.button({ text = noctalia.tr("collector.install"), variant = "primary",
controlSize = "sm", onClick = "onInstallCollectorClicked" }))
elseif enabled and status == "upgrade-required" then
table.insert(actions, 1, ui.button({ text = noctalia.tr("collector.upgrade"), variant = "primary",
controlSize = "sm", onClick = "onInstallCollectorClicked" }))
elseif enabled and status == "stale" and collector.enable_command ~= nil then
table.insert(actions, 1, ui.button({ text = noctalia.tr("collector.start"), variant = "primary",
controlSize = "sm", onClick = "onStartCollectorClicked" }))
elseif status == "healthy" and collector.disable_command ~= nil then
table.insert(actions, 1, ui.button({ text = noctalia.tr("collector.pause"), variant = "ghost",
controlSize = "sm", onClick = "onPauseCollectorClicked" }))
elseif not enabled and collector.installed and collector.disable_command ~= nil then
table.insert(actions, 1, ui.button({ text = noctalia.tr("collector.stop_service"), variant = "ghost",
controlSize = "sm", onClick = "onPauseCollectorClicked" }))
end
if collector.installed and collector.interval_command ~= nil then
table.insert(actions, 1, ui.button({
text = noctalia.tr("collector.apply_interval", {
minutes = tostring(collector.smart_refresh_minutes or 15),
}), variant = "outline", controlSize = "sm", enabled = (intervalApply == nil
or intervalApply.state ~= "authorizing") and (privilegedAction == nil
or privilegedAction.state ~= "authorizing"), onClick = "onApplyCollectorIntervalClicked",
}))
end
local children = {
ui.row({ gap = 8, align = "center" }, {
ui.glyph({ name = "shield-cog", size = 17, color = color }),
ui.column({ gap = 1, flexGrow = 1 }, {
ui.label({ text = noctalia.tr("collector.settings_title"), fontWeight = "bold", color = color }),
ui.label({
text = noctalia.tr("collector.status_" .. status:gsub("%-", "_"), {
version = tostring(collector.version or collector.expected_version or ""),
}),
fontSize = 11, color = "on_surface_variant", maxLines = 2,
}),
}),
}),
ui.row({ gap = 8, padding = 8, radius = 8, fill = "surface/0.42", align = "center" }, {
ui.glyph({ name = "server", size = 14, color = "on_surface_variant" }),
ui.column({ gap = 2, flexGrow = 1 }, {
ui.label({ text = noctalia.tr("collector.basic_title"), fontWeight = "bold", fontSize = 11 }),
ui.label({ text = noctalia.tr("collector.basic_features"), fontSize = 10,
color = "on_surface_variant", maxLines = 2 }),
}),
}),
ui.row({ gap = 8, padding = 8, radius = 8, fill = "primary/0.10", align = "center" }, {
ui.glyph({ name = "shield-check", size = 14, color = "primary" }),
ui.column({ gap = 2, flexGrow = 1 }, {
ui.label({ text = noctalia.tr("collector.full_title"), fontWeight = "bold", fontSize = 11,
color = "primary" }),
ui.label({ text = noctalia.tr("collector.full_features"), fontSize = 10,
color = "on_surface_variant", maxLines = 3 }),
}),
}),
ui.label({ text = noctalia.tr("collector.settings_hint"), fontSize = 10,
color = "on_surface_variant", maxLines = 2 }),
ui.label({ text = noctalia.tr("collector.interval", {
minutes = tostring(collector.smart_refresh_minutes or 15),
}), fontSize = 10, color = "on_surface_variant" }),
ui.row({ gap = 7, align = "center" }, actions),
}
if confirmUninstall then
table.insert(children, ui.label({ text = noctalia.tr("collector.remove_confirm"),
fontSize = 10, color = "error" }))
end
if intervalApply ~= nil then
table.insert(children, ui.label({
text = intervalApply.state == "authorizing" and noctalia.tr("collector.interval_authorizing")
or noctalia.tr("collector.interval_failed", { error = tostring(intervalApply.error or "") }),
fontSize = 10, color = intervalApply.state == "authorizing" and "secondary" or "error", maxLines = 2,
}))
end
if privilegedAction ~= nil and privilegedAction.scope == "collector" then
local action = noctalia.tr("collector.action_" .. privilegedAction.kind)
table.insert(children, ui.label({
text = privilegedAction.state == "authorizing"
and noctalia.tr("privileged_action.authorizing", { action = action })
or noctalia.tr("privileged_action.failed", { action = action,
error = tostring(privilegedAction.error or "") }),
fontSize = 10,
color = privilegedAction.state == "authorizing" and "secondary" or "error",
maxLines = 2,
}))
end
if collector.installed then
table.insert(children, ui.button({ text = noctalia.tr("collector.remove"),
variant = confirmUninstall and "destructive" or "ghost", controlSize = "sm",
onClick = "onUninstallCollectorClicked" }))
end
return ui.column({
gap = 8, padding = 10, radius = 10, fill = "surface_variant/0.24",
border = "outline/0.38", borderWidth = 1,
}, children)
end
local function trendCard(drive)
local historyDrives = type(history.drives) == "table" and history.drives or {}
local entry = historyDrives[driveId(drive)]
local samples = type(entry) == "table" and entry.samples or {}
if #samples < MIN_TREND_SAMPLES then
return nil
end
local temperatures = {}
local life = {}
local hasTemperatures = true
local hasLife = true
for _, sample in ipairs(samples) do
local temperature = tonumber(sample.hotspot_temperature_c or sample.temperature_c)
local remaining = tonumber(sample.remaining_life_percent)
if temperature == nil then
hasTemperatures = false
else
table.insert(temperatures, clamp(temperature / 100, 0, 1))
end
if remaining == nil then
hasLife = false
else
table.insert(life, clamp(remaining / 100, 0, 1))
end
end
if not hasTemperatures then temperatures = {} end
if not hasLife then life = {} end
if #temperatures < MIN_TREND_SAMPLES and #life < MIN_TREND_SAMPLES then
return nil
end
local primaryValues = #temperatures >= MIN_TREND_SAMPLES and temperatures or life
local secondaryValues = #temperatures >= MIN_TREND_SAMPLES and #life >= MIN_TREND_SAMPLES and life or nil
local primaryColor = #temperatures >= MIN_TREND_SAMPLES and "secondary" or "primary"
local legend = {}
if #temperatures >= MIN_TREND_SAMPLES then
table.insert(legend, ui.label({ text = "● " .. noctalia.tr("history.hotspot"), fontSize = 10, color = "secondary" }))
end
if #life >= MIN_TREND_SAMPLES then
table.insert(legend, ui.label({ text = "● " .. noctalia.tr("history.life"), fontSize = 10, color = "primary" }))
end
return ui.column({ gap = 5, padding = 8, radius = 9, fill = "surface/0.38" }, {
ui.row({ justify = "space_between", align = "center" }, {
ui.label({ text = noctalia.tr("history.title"), fontSize = 11, fontWeight = "bold" }),
ui.label({ text = noctalia.tr("history.samples", { count = #samples }), fontSize = 10, color = "on_surface_variant" }),
}),
ui.graph({ values = primaryValues, values2 = secondaryValues, color = primaryColor, color2 = "primary",
lineWidth = 2, fillOpacity = 0.10, height = 44 }),
ui.row({ gap = 12, align = "center" }, legend),
})
end
local function selfTestCard(drive)
local firmwareState = tostring(drive.self_test_state or "unsupported")
local launch = selfTestLaunch ~= nil and selfTestLaunch.drive_id == driveId(drive) and selfTestLaunch or nil
local state = launch ~= nil and launch.state ~= "failed" and firmwareState ~= "running"
and launch.state or firmwareState
local color = state == "failed" and "error"
or ((state == "running" or state == "authorizing" or state == "starting") and "secondary" or "primary")
local collector = snapshot and snapshot.system_collector or {}
local helper = collector.helper_available == true
local authorization = collector.authorization_available == true
local canStart = helper and authorization
local pending = pendingSelfTest ~= nil and pendingSelfTest.drive_id == driveId(drive)
local completion = tonumber(drive.self_test_completion_percent)
local status = drive.self_test_status or noctalia.tr("self_test.unavailable")
if launch ~= nil and firmwareState ~= "running" then
if launch.state == "authorizing" then
status = noctalia.tr("self_test.authorizing")
elseif launch.state == "starting" then
status = noctalia.tr("self_test.starting")
elseif launch.state == "failed" then
status = launch.error or noctalia.tr("self_test.launch_failed")
end
end
local children = {
ui.row({ gap = 8, align = "center" }, {
ui.glyph({ name = "stethoscope", size = 15, color = color }),
ui.column({ gap = 1, flexGrow = 1 }, {
ui.label({ text = noctalia.tr("self_test.title"), fontSize = 11, fontWeight = "bold" }),
ui.label({ text = tostring(status),
fontSize = 10, color = "on_surface_variant" }),
}),
}),
}
if firmwareState == "running" and completion ~= nil then
table.insert(children, ui.column({ gap = 4 }, {
ui.row({ justify = "space_between", align = "center" }, {
ui.label({ text = noctalia.tr("self_test.progress"), fontSize = 10, color = "on_surface_variant" }),
ui.label({ text = formatPercent(completion), fontSize = 10, fontWeight = "bold", color = color }),
}),
ui.progress({ progress = clamp(completion / 100, 0, 1), fill = color, height = 5 }),
}))
end
if drive.self_test_supported then
if pending then
table.insert(children, ui.label({
text = noctalia.tr("self_test.confirm", { type = noctalia.tr("self_test." .. pendingSelfTest.kind) }),
fontSize = 11, color = "secondary",
}))
table.insert(children, ui.row({ gap = 7 }, {
ui.button({ text = noctalia.tr("self_test.confirm_action"), variant = "primary", controlSize = "sm",
onClick = "onConfirmSelfTestClicked" }),
ui.button({ text = noctalia.tr("self_test.cancel"), variant = "ghost", controlSize = "sm",
onClick = "onCancelSelfTestClicked" }),
}))
else
table.insert(children, ui.row({ gap = 7 }, {
ui.button({ text = noctalia.tr("self_test.short"), variant = "outline", controlSize = "sm",
enabled = canStart and firmwareState ~= "running" and state ~= "authorizing" and state ~= "starting",
onClick = "onStartShortSelfTestClicked" }),
ui.button({ text = noctalia.tr("self_test.long"), variant = "outline", controlSize = "sm",
enabled = canStart and firmwareState ~= "running" and state ~= "authorizing" and state ~= "starting",
onClick = "onStartLongSelfTestClicked" }),
}))
if not helper then
table.insert(children, ui.label({ text = noctalia.tr("self_test.helper_required"),
fontSize = 10, color = "on_surface_variant" }))
elseif not authorization then
table.insert(children, ui.label({ text = noctalia.tr("self_test.authorization_required"),
fontSize = 10, color = "error" }))
end
end
end
return ui.column({ gap = 7, padding = 9, radius = 9, fill = color .. "/0.08",
border = color .. "/0.20", borderWidth = 1 }, children)
end
local function preferenceEditor(drive)
local thresholdFields = {
ui.column({ gap = 3, flexGrow = 1 }, {
ui.label({ text = noctalia.tr("preferences.warning_temperature"), fontSize = 10, color = "on_surface_variant" }),
ui.input({ key = driveId(drive) .. "-warning", value = warningDraft, placeholder = "65",
controlSize = "sm", onChange = "onWarningThresholdChanged" }),
}),
ui.column({ gap = 3, flexGrow = 1 }, {
ui.label({ text = noctalia.tr("preferences.critical_temperature"), fontSize = 10, color = "on_surface_variant" }),
ui.input({ key = driveId(drive) .. "-critical", value = criticalDraft, placeholder = "80",
controlSize = "sm", onChange = "onCriticalThresholdChanged" }),
}),
}
if drive.kind ~= "hdd" then
table.insert(thresholdFields, ui.column({ gap = 3, flexGrow = 1 }, {
ui.label({ text = noctalia.tr("preferences.life_warning"), fontSize = 10, color = "on_surface_variant" }),
ui.input({ key = driveId(drive) .. "-life", value = lifeDraft, placeholder = "20",
controlSize = "sm", onChange = "onLifeThresholdChanged" }),
}))
end
return ui.column({ gap = 8, padding = 10, radius = 9, fill = "primary/0.07",
border = "primary/0.24", borderWidth = 1 }, {
ui.row({ justify = "space_between", align = "center" }, {
ui.label({ text = noctalia.tr("preferences.title"), fontWeight = "bold", color = "primary" }),
ui.row({ gap = 5 }, {
ui.button({ glyph = "arrow-up", tooltip = noctalia.tr("preferences.move_up"), variant = "ghost",
controlSize = "sm", onClick = "onMoveDriveUpClicked" }),
ui.button({ glyph = "arrow-down", tooltip = noctalia.tr("preferences.move_down"), variant = "ghost",
controlSize = "sm", onClick = "onMoveDriveDownClicked" }),
}),
}),
ui.label({ text = noctalia.tr("preferences.alias"), fontSize = 10, color = "on_surface_variant" }),
ui.input({ key = driveId(drive) .. "-alias", value = aliasDraft,
placeholder = drive.model or drive.device, controlSize = "sm", onChange = "onAliasChanged" }),
ui.row({ gap = 8, align = "center" }, thresholdFields),
ui.row({ gap = 12, align = "center" }, {
ui.toggle({ checked = alertsDraft, onChange = "onDriveAlertsChanged" }),
ui.label({ text = noctalia.tr("preferences.alerts"), fontSize = 11, flexGrow = 1 }),
ui.toggle({ checked = presenceDraft, onChange = "onPresenceAlertsChanged" }),
ui.label({ text = noctalia.tr("preferences.presence"), fontSize = 11, flexGrow = 1 }),
}),
ui.row({ gap = 7, justify = "end" }, {
ui.button({ text = noctalia.tr("preferences.hide"), variant = "ghost", controlSize = "sm",
onClick = "onHideDriveClicked" }),
ui.button({ text = noctalia.tr("self_test.cancel"), variant = "ghost", controlSize = "sm",
onClick = "onCancelDrivePreferencesClicked" }),
ui.button({ text = noctalia.tr("preferences.save"), variant = "primary", controlSize = "sm",
onClick = "onSaveDrivePreferencesClicked" }),
}),
})
end
local function driveCard(drive, index)
local id = driveId(drive)
local expanded = expandedDriveId == id
local isHdd = drive.kind == "hdd"
local healthText, healthColor, healthIcon = healthInfo(drive)
local temperature = tonumber(drive.temperature_c)
local hotspot = tonumber(drive.hotspot_temperature_c) or temperature
local alertTemperature = noctalia.getConfig("use_hotspot_temperature") == false and temperature or hotspot
local tempColor = temperatureColor(alertTemperature, drive)
local tempText = hotspot ~= nil and string.format("%.0f °C", hotspot) or noctalia.tr("common.not_available")
if temperature ~= nil and hotspot ~= nil and hotspot > temperature then
tempText = noctalia.tr("metrics.hotspot_and_composite", {
hotspot = string.format("%.0f °C", hotspot),
composite = string.format("%.0f °C", temperature),
})
end
local life = tonumber(drive.remaining_life_percent)
local lifeText = formatPercent(life)
if drive.remaining_life_estimated then
lifeText = "~" .. lifeText
end
local lifeWarning = number(drive.life_warning_percent,
number(noctalia.getConfig("life_warning_percent"), 20))
local lifeColor = life ~= nil and (life <= 10 and "error"
or (life <= lifeWarning and "secondary" or "primary")) or "on_surface_variant"
local storage = tonumber(drive.storage_usage_percent)
local storageColor = storage ~= nil and (storage >= 95 and "error" or (storage >= 85 and "secondary" or "primary")) or "on_surface_variant"
local storageText = storage ~= nil and formatPercent(storage) or noctalia.tr("storage.not_mounted")
local transport = string.upper(tostring(drive.transport or "unknown"))
local device = tostring(drive.device or "")
local details = {
metric("temperature", noctalia.tr("metrics.temperature"), tempText, tempColor),
metric(healthIcon, noctalia.tr("metrics.smart_health"), healthText, healthColor),
}
local progressRows = {}
if not isHdd then
table.insert(progressRows, progressMetric(
drive.remaining_life_estimated and noctalia.tr("metrics.life_remaining_estimated") or noctalia.tr("metrics.life_remaining"),
life,
lifeText,
lifeColor
))
end
table.insert(progressRows, progressMetric(noctalia.tr("metrics.storage_used"), storage, storageText, storageColor))
local ioDetails = {}
if drive.data_written_bytes ~= nil then
table.insert(ioDetails, metric("database-export", noctalia.tr("metrics.data_written"), formatBytes(drive.data_written_bytes)))
end
if drive.data_read_bytes ~= nil then
table.insert(ioDetails, metric("database-import", noctalia.tr("metrics.data_read"), formatBytes(drive.data_read_bytes)))
end
local wearDetails = {}
if drive.percentage_used ~= nil then
table.insert(wearDetails, metric(
"gauge",
noctalia.tr("metrics.endurance_used"),
formatPercent(drive.percentage_used),
number(drive.percentage_used, 0) >= 90 and "error" or "on_surface"
))
end
if drive.available_spare_percent ~= nil then
local spare = number(drive.available_spare_percent, 100)
local spareThreshold = number(drive.available_spare_threshold_percent, 10)
table.insert(wearDetails, metric(
"shield-check",
noctalia.tr("metrics.available_spare"),
formatPercent(drive.available_spare_percent),
spare <= math.max(5, spareThreshold / 2) and "error"
or (spare <= spareThreshold and "secondary" or "on_surface")
))
end
local powerDetails = {
metric("clock", noctalia.tr("metrics.power_on"), formatPowerOn(drive)),
}
local thermalDetails = {}
if type(drive.temperature_sensors_c) == "table" and #drive.temperature_sensors_c > 0 then
local sensors = {}
for sensorIndex, sensor in ipairs(drive.temperature_sensors_c) do
table.insert(sensors, string.format("S%d %.0f°", sensorIndex, number(sensor, 0)))
end
table.insert(thermalDetails, metric("temperature-sun", noctalia.tr("metrics.temperature_sensors"),
table.concat(sensors, " · "), tempColor))
end
if drive.device_warning_temperature_c ~= nil or drive.device_critical_temperature_c ~= nil then
local warningLimit = tonumber(drive.device_warning_temperature_c)
local criticalLimit = tonumber(drive.device_critical_temperature_c)
table.insert(thermalDetails, metric("temperature-cog", noctalia.tr("metrics.device_temperature_limits"),
(warningLimit ~= nil and string.format("%.0f°", warningLimit) or noctalia.tr("common.not_available"))
.. " / "
.. (criticalLimit ~= nil and string.format("%.0f°", criticalLimit) or noctalia.tr("common.not_available"))))
end
if drive.power_cycles ~= nil then
table.insert(powerDetails, metric("repeat", noctalia.tr("metrics.power_cycles"), tostring(math.floor(number(drive.power_cycles, 0)))))
end
if drive.start_stop_count ~= nil then
table.insert(powerDetails, metric("player-play", noctalia.tr("metrics.start_stop_count"),
tostring(math.floor(number(drive.start_stop_count, 0)))))
end
if drive.load_cycle_count ~= nil then
table.insert(powerDetails, metric("refresh", noctalia.tr("metrics.load_cycle_count"),
tostring(math.floor(number(drive.load_cycle_count, 0)))))
end
local errorDetails = {}
if drive.unsafe_shutdowns ~= nil then
table.insert(errorDetails, metric(
"bolt-off",
noctalia.tr("metrics.unsafe_shutdowns"),
tostring(math.floor(number(drive.unsafe_shutdowns, 0))),
number(drive.unsafe_shutdowns, 0) > 0 and "secondary" or "on_surface"
))
end
if drive.media_errors ~= nil then
table.insert(errorDetails, metric(
"alert-circle",
noctalia.tr("metrics.media_errors"),
tostring(math.floor(number(drive.media_errors, 0))),
number(drive.media_errors, 0) > 0 and "error" or "on_surface"
))
end
local integrityDetails = {}
if drive.error_log_entries ~= nil and drive.error_log_entries ~= drive.media_errors then
table.insert(integrityDetails, metric(
"file-alert",
noctalia.tr("metrics.error_log_entries"),
tostring(math.floor(number(drive.error_log_entries, 0))),
number(drive.error_log_entries, 0) > 0 and "secondary" or "on_surface"
))
end
if drive.critical_warning ~= nil then
table.insert(integrityDetails, metric(
"alert-octagon",
noctalia.tr("metrics.critical_warning"),
string.format("0x%02X", math.floor(number(drive.critical_warning, 0))),
number(drive.critical_warning, 0) > 0 and "error" or "primary"
))
end
if drive.reallocated_sectors ~= nil then
table.insert(integrityDetails, metric(
"replace",
noctalia.tr("metrics.reallocated"),
tostring(math.floor(number(drive.reallocated_sectors, 0))),
number(drive.reallocated_sectors, 0) > 0 and "error" or "primary"
))
end
if drive.pending_sectors ~= nil then
table.insert(integrityDetails, metric(
"hourglass",
noctalia.tr("metrics.pending"),
tostring(math.floor(number(drive.pending_sectors, 0))),
number(drive.pending_sectors, 0) > 0 and "error" or "primary"
))
end
if drive.uncorrectable_errors ~= nil then
table.insert(integrityDetails, metric(
"alert-hexagon",
noctalia.tr("metrics.uncorrectable"),
tostring(math.floor(number(drive.uncorrectable_errors, 0))),
number(drive.uncorrectable_errors, 0) > 0 and "error" or "primary"
))
end
if drive.spin_retry_count ~= nil then
table.insert(integrityDetails, metric(
"repeat",
noctalia.tr("metrics.spin_retry_count"),
tostring(math.floor(number(drive.spin_retry_count, 0))),
number(drive.spin_retry_count, 0) > 0 and "error" or "primary"
))
end
if drive.command_timeout_count ~= nil then
table.insert(integrityDetails, metric(
"clock-exclamation",
noctalia.tr("metrics.command_timeout_count"),
tostring(math.floor(number(drive.command_timeout_count, 0))),
number(drive.command_timeout_count, 0) > 0 and "secondary" or "primary"
))
end
if drive.interface_crc_errors ~= nil then
table.insert(integrityDetails, metric(
"link-off",
noctalia.tr("metrics.interface_crc_errors"),
tostring(math.floor(number(drive.interface_crc_errors, 0))),
number(drive.interface_crc_errors, 0) > 0 and "secondary" or "primary"
))
end
local identity = {
ui.label({ text = driveName(drive), fontWeight = "bold", color = "on_surface" }),
ui.label({
text = transport .. " • " .. formatBytes(drive.capacity_bytes) .. " • " .. device,
fontSize = 11,
color = "on_surface_variant",
}),
}
local compactMetrics = {
metric("temperature", isHdd and noctalia.tr("metrics.temperature") or noctalia.tr("metrics.hotspot_temperature"),
tempText, tempColor),
isHdd and metric("clock", noctalia.tr("metrics.power_on"), formatPowerOn(drive))
or metric("battery-vertical", noctalia.tr("metrics.life_remaining"), lifeText, lifeColor),
metric(healthIcon, noctalia.tr("metrics.smart_health"), healthText, healthColor),
}
local children = {
ui.row({ gap = 10, align = "center" }, {
ui.column({ width = 36, height = 36, radius = 10, fill = tempColor .. "/0.16", align = "center", justify = "center" }, {
ui.glyph({ name = drive.kind == "ssd" and "server" or "disc", size = 19, color = tempColor }),
}),
ui.column({ gap = 2, flexGrow = 1 }, identity),
ui.row({ fill = healthColor .. "/0.16", radius = 8, paddingH = 8, paddingV = 4, align = "center", gap = 4 }, {
ui.glyph({ name = healthIcon, size = 12, color = healthColor }),
ui.label({ text = healthText, fontSize = 11, fontWeight = "bold", color = healthColor }),
}),
ui.button({
glyph = expanded and "chevron-up" or "chevron-down", variant = "ghost", controlSize = "sm",
tooltip = noctalia.tr(expanded and "panel.collapse" or "panel.expand"),
enabled = index <= 32, onClick = "onDrive" .. tostring(index) .. "Clicked",
}),
}),
ui.row({ gap = 14, align = "center" }, compactMetrics),
}
if expanded then
if drive.serial ~= nil and drive.serial ~= "" then
table.insert(children, ui.label({
text = noctalia.tr("metrics.serial", { value = tostring(drive.serial) }),
fontSize = 10, color = "on_surface_variant/0.78", maxLines = 1,
}))
end
if type(drive.mount_points) == "table" and #drive.mount_points > 0 then
table.insert(children, ui.label({
text = noctalia.tr("metrics.mounted_at", { paths = table.concat(drive.mount_points, " · ") }),
fontSize = 10, color = "primary/0.88", maxLines = 2,
}))
end
table.insert(children, ui.row({ gap = 16, align = "center" }, details))
table.insert(children, ui.row({ gap = 16, align = "center" }, progressRows))
appendMetricRows(children, wearDetails)
appendMetricRows(children, ioDetails)
appendMetricRows(children, powerDetails)
appendMetricRows(children, thermalDetails)
appendMetricRows(children, errorDetails)
appendMetricRows(children, integrityDetails)
local trends = trendCard(drive)
if trends ~= nil then
table.insert(children, trends)
end
table.insert(children, selfTestCard(drive))
table.insert(children, ui.row({ gap = 7, justify = "end" }, {
ui.button({ text = noctalia.tr("preferences.edit"), glyph = "settings", variant = "ghost",
controlSize = "sm", onClick = "onEditExpandedDriveClicked" }),
}))
end
if drive.smart_sleeping then
table.insert(children, ui.row({ gap = 7, align = "center", fill = "primary/0.08", radius = 8, padding = 8 }, {
ui.glyph({ name = "moon", size = 13, color = "primary" }),
ui.label({ text = noctalia.tr("smart.sleeping"), fontSize = 11, color = "on_surface_variant", flexGrow = 1 }),
}))
elseif not drive.smart_available then
table.insert(children, ui.row({ gap = 7, align = "center", fill = "secondary/0.12", radius = 8, padding = 8 }, {
ui.glyph({ name = "info-circle", size = 13, color = "secondary" }),
ui.label({ text = noctalia.tr("smart.access_limited"), fontSize = 11, color = "on_surface_variant", flexGrow = 1 }),
}))
end
if expanded and drive.smart_completeness == "partial" then
table.insert(children, ui.row({ gap = 7, align = "center", fill = "secondary/0.12", radius = 8, padding = 8 }, {
ui.glyph({ name = "file-alert", size = 13, color = "secondary" }),
ui.label({ text = noctalia.tr("smart.partial_details"), fontSize = 11, color = "on_surface_variant", flexGrow = 1 }),
}))
for _, message in ipairs(drive.smart_messages or {}) do
table.insert(children, ui.label({ text = "• " .. tostring(message.message or message),
fontSize = 10, color = "on_surface_variant", maxLines = 3 }))
end
end
if expanded and editingDriveId == id then
table.insert(children, preferenceEditor(drive))
end
return ui.column({
key = tostring(drive.id or device),
gap = 12,
padding = 14,
radius = 14,
fill = "surface_variant/0.34",
border = "outline/0.38",
borderWidth = 1,
align = "stretch",
}, children)
end
local function visibleDrives()
local drives = {}
local showHdd = noctalia.getConfig("show_hdd") ~= false
if snapshot ~= nil and type(snapshot.disks) == "table" then
for _, drive in ipairs(snapshot.disks) do
if (drive.kind == "ssd" or showHdd) and drive.hidden ~= true then
table.insert(drives, drive)
end
end
end
return drives
end
local function hiddenDrivesCard()
local hidden = {}
if snapshot ~= nil and type(snapshot.disks) == "table" then
for _, drive in ipairs(snapshot.disks) do
if drive.hidden == true then
table.insert(hidden, drive)
end
end
end
if #hidden == 0 then
return nil
end
hiddenSelection = math.max(1, math.min(hiddenSelection, #hidden))
local options = {}
for _, drive in ipairs(hidden) do
table.insert(options, driveName(drive))
end
return ui.column({ gap = 7, padding = 10, radius = 9, fill = "surface_variant/0.22" }, {
ui.label({ text = noctalia.tr("preferences.hidden_drives"), fontWeight = "bold", fontSize = 11 }),
ui.row({ gap = 7, align = "center" }, {
ui.select({ options = options, selectedIndex = hiddenSelection - 1, controlSize = "sm",
flexGrow = 1, onChange = "onHiddenDriveSelected" }),
ui.button({ text = noctalia.tr("preferences.restore"), variant = "outline", controlSize = "sm",
onClick = "onRestoreHiddenDriveClicked" }),
}),
})
end
local function render()
if not opened then
return
end
local summary = snapshot and snapshot.summary or {}
local drives = visibleDrives()
currentDrives = drives
currentIssues = type(snapshot and snapshot.issues) == "table" and snapshot.issues or {}
local includeHdd = noctalia.getConfig("show_hdd") ~= false
local hottest = tonumber(includeHdd and summary.hottest_drive_temperature_c or summary.hottest_ssd_temperature_c)
local hottestDriveName = includeHdd and summary.hottest_drive_name or summary.hottest_ssd_drive_name
local remaining = tonumber(summary.worst_ssd_remaining_life_percent)
local lowestLifeDriveName = summary.worst_ssd_life_drive_name
local ssdCount = number(summary.ssd_count, 0)
local hddCount = includeHdd and number(summary.hdd_count, 0) or 0
local driveCount = ssdCount + hddCount
local smartAvailable = includeHdd and number(summary.smart_available_count,
number(summary.ssd_smart_available_count, 0) + number(summary.hdd_smart_available_count, 0))
or number(summary.ssd_smart_available_count, 0)
local headerStatus = snapshot and snapshot.collecting and noctalia.tr("panel.refreshing") or noctalia.tr("panel.updated", {
time = tostring(snapshot and snapshot.generated_at_local or noctalia.tr("common.never")),
})
local body = {}
if snapshot == nil then
table.insert(body, ui.column({ gap = 10, align = "center", justify = "center", flexGrow = 1 }, {
ui.glyph({ name = "server-off", size = 36, color = "on_surface_variant" }),
ui.label({ text = noctalia.tr("panel.waiting"), color = "on_surface_variant" }),
}))
else
local dependencyStatus = dependencyCard(snapshot.dependencies)
if dependencyStatus ~= nil then
table.insert(body, dependencyStatus)
end
local collectorStatus = collectorCard(snapshot.system_collector)
if collectorStatus ~= nil then
table.insert(body, collectorStatus)
end
local collectorSettings = collectorSettingsCard(snapshot.system_collector)
if collectorSettings ~= nil then
table.insert(body, collectorSettings)
end
local hottestColor = temperatureColor(hottest)
local summaryCards = {
summaryCard(tostring(driveCount), noctalia.tr("summary.drives"),
noctalia.tr("summary.drive_mix", { ssds = ssdCount, hdds = hddCount }), "primary"),
summaryCard(hottest ~= nil and string.format("%.0f °C", hottest) or "-- °C",
noctalia.tr("summary.hottest"), hottestDriveName, hottestColor),
}
if ssdCount > 0 then
table.insert(summaryCards, summaryCard(formatPercent(remaining),
noctalia.tr("summary.lowest_ssd_life"), lowestLifeDriveName, "secondary"))
end
table.insert(body, ui.row({ gap = 10, align = "stretch" }, summaryCards))
local alerts = alertsCard(currentIssues)
if alerts ~= nil then
table.insert(body, alerts)
end
local sleeping = number(summary.sleeping_count, 0)
if smartAvailable + sleeping < driveCount then
table.insert(body, ui.row({ gap = 8, padding = 10, radius = 10, fill = "secondary/0.12", align = "center" }, {
ui.glyph({ name = "shield-lock", size = 16, color = "secondary" }),
ui.column({ gap = 2, flexGrow = 1 }, {
ui.label({ text = noctalia.tr("smart.partial_title"), fontWeight = "bold", color = "secondary" }),
ui.label({ text = noctalia.tr("smart.partial_body", { available = smartAvailable,
total = driveCount - sleeping }), fontSize = 11, color = "on_surface_variant" }),
}),
}))
end
if snapshot.collector_error ~= nil and snapshot.collector_error ~= "" then
table.insert(body, ui.label({ text = tostring(snapshot.collector_error), color = "error", fontSize = 11 }))
end
if #drives == 0 then
table.insert(body, ui.label({ text = noctalia.tr("panel.no_drives"), color = "on_surface_variant" }))
else
for index, drive in ipairs(drives) do
table.insert(body, driveCard(drive, index))
end
end
local hiddenStatus = hiddenDrivesCard()
if hiddenStatus ~= nil then
table.insert(body, hiddenStatus)
end
end
panel.render(ui.column({ flexGrow = 1, gap = 12, align = "stretch" }, {
ui.row({ align = "center", gap = 10 }, {
ui.column({ width = 38, height = 38, radius = 11, fill = "primary/0.16", align = "center", justify = "center" }, {
ui.glyph({ name = "server-2", size = 20, color = "primary" }),
}),
ui.column({ gap = 1, flexGrow = 1 }, {
ui.label({ text = noctalia.tr("panel.title"), fontSize = 17, fontWeight = "bold", color = "on_surface" }),
ui.label({ text = headerStatus, fontSize = 11, color = "on_surface_variant" }),
}),
ui.button({ glyph = "settings", variant = showCollectorSettings and "primary" or "ghost",
tooltip = noctalia.tr("collector.settings_title"), onClick = "onToggleCollectorSettingsClicked" }),
ui.button({ glyph = "refresh", variant = "ghost", onClick = "onRefreshClicked" }),
ui.button({ glyph = "x", variant = "ghost", onClick = "onCloseClicked" }),
}),
ui.scroll({ flexGrow = 1, gap = 12 }, body),
}))
end
local function selfTestSignature(drive)
if drive == nil then return "" end
return table.concat({
tostring(drive.self_test_state or ""),
tostring(drive.self_test_status or ""),
tostring(drive.self_test_lifetime_hours or ""),
tostring(drive.self_test_error_count or ""),
}, "|")
end
local function reconcileSelfTestLaunch(value)
if selfTestLaunch == nil or selfTestLaunch.state == "authorizing" or selfTestLaunch.state == "failed" then
return
end
local drive = nil
for _, candidate in ipairs(value and value.disks or {}) do
if driveId(candidate) == selfTestLaunch.drive_id then
drive = candidate
break
end
end
if drive == nil then return end
if tostring(drive.self_test_state) == "running" then
selfTestLaunch.observed_running = true
return
end
local generated = number(value and value.generated_at_epoch, 0)
local changed = generated > number(selfTestLaunch.accepted_snapshot_epoch, 0)
and selfTestSignature(drive) ~= tostring(selfTestLaunch.baseline_signature or "")
local expired = os.time() - number(selfTestLaunch.accepted_at, os.time()) >= 180
if selfTestLaunch.observed_running or changed or expired then
selfTestLaunch = nil
end
end
noctalia.state.watch("snapshot", function(value)
snapshot = value
reconcileSelfTestLaunch(value)
render()
end)
noctalia.state.watch("drive_history", function(value)
history = type(value) == "table" and value or { drives = {} }
render()
end)
noctalia.state.watch("drive_preferences", function(value)
if type(value) == "table" then
preferences = value
end
render()
end)
local function toggleDriveAt(index)
local drive = currentDrives[index]
if drive == nil then
return
end
local id = driveId(drive)
if expandedDriveId == id then
expandedDriveId = nil
editingDriveId = nil
pendingSelfTest = nil
else
expandedDriveId = id
end
render()
end
function onDrive1Clicked() toggleDriveAt(1) end
function onDrive2Clicked() toggleDriveAt(2) end
function onDrive3Clicked() toggleDriveAt(3) end
function onDrive4Clicked() toggleDriveAt(4) end
function onDrive5Clicked() toggleDriveAt(5) end
function onDrive6Clicked() toggleDriveAt(6) end
function onDrive7Clicked() toggleDriveAt(7) end
function onDrive8Clicked() toggleDriveAt(8) end
function onDrive9Clicked() toggleDriveAt(9) end
function onDrive10Clicked() toggleDriveAt(10) end
function onDrive11Clicked() toggleDriveAt(11) end
function onDrive12Clicked() toggleDriveAt(12) end
function onDrive13Clicked() toggleDriveAt(13) end
function onDrive14Clicked() toggleDriveAt(14) end
function onDrive15Clicked() toggleDriveAt(15) end
function onDrive16Clicked() toggleDriveAt(16) end
function onDrive17Clicked() toggleDriveAt(17) end
function onDrive18Clicked() toggleDriveAt(18) end
function onDrive19Clicked() toggleDriveAt(19) end
function onDrive20Clicked() toggleDriveAt(20) end
function onDrive21Clicked() toggleDriveAt(21) end
function onDrive22Clicked() toggleDriveAt(22) end
function onDrive23Clicked() toggleDriveAt(23) end
function onDrive24Clicked() toggleDriveAt(24) end
function onDrive25Clicked() toggleDriveAt(25) end
function onDrive26Clicked() toggleDriveAt(26) end
function onDrive27Clicked() toggleDriveAt(27) end
function onDrive28Clicked() toggleDriveAt(28) end
function onDrive29Clicked() toggleDriveAt(29) end
function onDrive30Clicked() toggleDriveAt(30) end
function onDrive31Clicked() toggleDriveAt(31) end
function onDrive32Clicked() toggleDriveAt(32) end
local function sendDismissRequest(request)
dismissRequestNonce += 1
request.nonce = dismissRequestNonce
noctalia.state.set("dismiss_alert_request", request)
end
local function dismissAlertAt(index)
local issue = currentIssues[index]
if issue ~= nil and issue.id ~= nil then
sendDismissRequest({ id = tostring(issue.id) })
end
end
function onDismissAlert1Clicked() dismissAlertAt(1) end
function onDismissAlert2Clicked() dismissAlertAt(2) end
function onDismissAlert3Clicked() dismissAlertAt(3) end
function onDismissAlert4Clicked() dismissAlertAt(4) end
function onDismissAlert5Clicked() dismissAlertAt(5) end
function onDismissAlert6Clicked() dismissAlertAt(6) end
function onDismissAlert7Clicked() dismissAlertAt(7) end
function onDismissAlert8Clicked() dismissAlertAt(8) end
function onDismissAlert9Clicked() dismissAlertAt(9) end
function onDismissAlert10Clicked() dismissAlertAt(10) end
function onDismissAlert11Clicked() dismissAlertAt(11) end
function onDismissAlert12Clicked() dismissAlertAt(12) end
function onDismissAlert13Clicked() dismissAlertAt(13) end
function onDismissAlert14Clicked() dismissAlertAt(14) end
function onDismissAlert15Clicked() dismissAlertAt(15) end
function onDismissAlert16Clicked() dismissAlertAt(16) end
function onDismissAlert17Clicked() dismissAlertAt(17) end
function onDismissAlert18Clicked() dismissAlertAt(18) end
function onDismissAlert19Clicked() dismissAlertAt(19) end
function onDismissAlert20Clicked() dismissAlertAt(20) end
function onDismissAlert21Clicked() dismissAlertAt(21) end
function onDismissAlert22Clicked() dismissAlertAt(22) end
function onDismissAlert23Clicked() dismissAlertAt(23) end
function onDismissAlert24Clicked() dismissAlertAt(24) end
function onDismissAlert25Clicked() dismissAlertAt(25) end
function onDismissAlert26Clicked() dismissAlertAt(26) end
function onDismissAlert27Clicked() dismissAlertAt(27) end
function onDismissAlert28Clicked() dismissAlertAt(28) end
function onDismissAlert29Clicked() dismissAlertAt(29) end
function onDismissAlert30Clicked() dismissAlertAt(30) end
function onDismissAlert31Clicked() dismissAlertAt(31) end
function onDismissAlert32Clicked() dismissAlertAt(32) end
function onDismissAllAlertsClicked()
if #currentIssues > 0 then
sendDismissRequest({ all = true })
end
end
local function expandedDrive()
for _, drive in ipairs(currentDrives) do
if driveId(drive) == expandedDriveId then
return drive
end
end
return nil
end
local function setPreferenceDrafts(drive)
local entry = preferenceFor(driveId(drive))
aliasDraft = tostring(entry.alias or "")
warningDraft = entry.warning_temperature ~= nil and tostring(entry.warning_temperature) or ""
criticalDraft = entry.critical_temperature ~= nil and tostring(entry.critical_temperature) or ""
lifeDraft = entry.life_warning_percent ~= nil and tostring(entry.life_warning_percent) or ""
alertsDraft = entry.alerts_enabled ~= false
presenceDraft = entry.presence_alert_enabled == nil and drive.presence_alert_enabled == true
or entry.presence_alert_enabled == true
end
function onEditExpandedDriveClicked()
local drive = expandedDrive()
if drive == nil then return end
editingDriveId = driveId(drive)
setPreferenceDrafts(drive)
render()
end
function onAliasChanged(value) aliasDraft = tostring(value or "") end
function onWarningThresholdChanged(value) warningDraft = tostring(value or "") end
function onCriticalThresholdChanged(value) criticalDraft = tostring(value or "") end
function onLifeThresholdChanged(value) lifeDraft = tostring(value or "") end
function onDriveAlertsChanged(value)
if editingDriveId ~= nil then
alertsDraft = value == true or tostring(value) == "true"
render()
end
end
function onPresenceAlertsChanged(value)
if editingDriveId ~= nil then
presenceDraft = value == true or tostring(value) == "true"
render()
end
end
local function optionalThreshold(value, minimum, maximum)
local trimmed = noctalia.string.trim(tostring(value or ""))
if trimmed == "" then return nil end
local parsed = tonumber(trimmed)
if parsed == nil then return false end
return clamp(parsed, minimum, maximum)
end
function onSaveDrivePreferencesClicked()
if editingDriveId == nil then return end
local warning = optionalThreshold(warningDraft, 30, 100)
local critical = optionalThreshold(criticalDraft, 31, 110)
local life = optionalThreshold(lifeDraft, 1, 100)
if warning == false or critical == false or life == false or (warning ~= nil and critical ~= nil and critical <= warning) then
noctalia.notifyError(noctalia.tr("preferences.title"), noctalia.tr("preferences.invalid_thresholds"))
return
end
local entry = preferenceFor(editingDriveId)
local previous = {}
for _, key in ipairs(EDITABLE_PREFERENCE_FIELDS) do previous[key] = entry[key] end
entry.alias = noctalia.string.trim(aliasDraft)
entry.warning_temperature = warning
entry.critical_temperature = critical
entry.life_warning_percent = life
entry.alerts_enabled = alertsDraft
entry.presence_alert_enabled = presenceDraft
if savePreferences() then
editingDriveId = nil
noctalia.notify(noctalia.tr("preferences.title"), noctalia.tr("preferences.saved"))
else
for _, key in ipairs(EDITABLE_PREFERENCE_FIELDS) do entry[key] = previous[key] end
end
render()
end
function onCancelDrivePreferencesClicked()
editingDriveId = nil
render()
end
local function ensureOrder()
preferences.order = type(preferences.order) == "table" and preferences.order or {}
local seen = {}
for _, id in ipairs(preferences.order) do seen[tostring(id)] = true end
for _, drive in ipairs(snapshot and snapshot.disks or {}) do
local id = driveId(drive)
if not seen[id] then
table.insert(preferences.order, id)
seen[id] = true
end
end
end
local function moveEditingDrive(delta)
if editingDriveId == nil then return end
ensureOrder()
for index, id in ipairs(preferences.order) do
if tostring(id) == editingDriveId then
local destination = math.max(1, math.min(#preferences.order, index + delta))
preferences.order[index], preferences.order[destination] = preferences.order[destination], preferences.order[index]
if not savePreferences() then
preferences.order[index], preferences.order[destination] = preferences.order[destination], preferences.order[index]
render()
end
return
end
end
end
function onMoveDriveUpClicked() moveEditingDrive(-1) end
function onMoveDriveDownClicked() moveEditingDrive(1) end
function onHideDriveClicked()
if editingDriveId == nil then return end
local entry = preferenceFor(editingDriveId)
local previous = entry.hidden
entry.hidden = true
if savePreferences() then
editingDriveId = nil
expandedDriveId = nil
else
entry.hidden = previous
end
render()
end
function onHiddenDriveSelected(value)
hiddenSelection = math.max(1, (tonumber(value) or 0) + 1)
end
function onRestoreHiddenDriveClicked()
local hidden = {}
for _, drive in ipairs(snapshot and snapshot.disks or {}) do
if drive.hidden == true then table.insert(hidden, drive) end
end
local drive = hidden[hiddenSelection]
if drive ~= nil then
local entry = preferenceFor(driveId(drive))
local previous = entry.hidden
entry.hidden = false
if savePreferences() then
hiddenSelection = 1
else
entry.hidden = previous
end
render()
end
end
function onStartShortSelfTestClicked()
local drive = expandedDrive()
if drive ~= nil then
if selfTestLaunch ~= nil and selfTestLaunch.drive_id == driveId(drive) and selfTestLaunch.state == "failed" then
selfTestLaunch = nil
end
pendingSelfTest = { drive_id = driveId(drive), kind = "short", device = drive.smart_device or drive.device }
render()
end
end
function onStartLongSelfTestClicked()
local drive = expandedDrive()
if drive ~= nil then
if selfTestLaunch ~= nil and selfTestLaunch.drive_id == driveId(drive) and selfTestLaunch.state == "failed" then
selfTestLaunch = nil
end
pendingSelfTest = { drive_id = driveId(drive), kind = "long", device = drive.smart_device or drive.device }
render()
end
end
function onCancelSelfTestClicked()
pendingSelfTest = nil
render()
end
local function selfTestError(result)
if result.timedOut == true then
return noctalia.tr("self_test.authorization_timeout")
end
if tonumber(result.exitCode) == 126 then
return noctalia.tr("self_test.authorization_cancelled")
end
local detail = noctalia.string.trim(tostring(result.stderr or ""))
if detail == "" then detail = noctalia.string.trim(tostring(result.stdout or "")) end
if #detail > 240 then detail = detail:sub(1, 237) .. "..." end
return detail ~= "" and detail or noctalia.tr("self_test.launch_failed")
end
function onConfirmSelfTestClicked()
if pendingSelfTest == nil then return end
local request = pendingSelfTest
pendingSelfTest = nil
local drive = expandedDrive()
selfTestLaunch = {
drive_id = request.drive_id,
kind = request.kind,
state = "authorizing",
baseline_signature = selfTestSignature(drive),
}
render()
local command = "pkexec /usr/local/libexec/noctalia-drive-health/smart-action.sh "
.. shellQuote(request.kind) .. " " .. shellQuote(request.device)
local launched = noctalia.runAsync(command, function(result)
if selfTestLaunch == nil or selfTestLaunch.drive_id ~= request.drive_id then return end
local exitCode = tonumber(result.exitCode)
-- smartctl bits 3-7 report existing drive-health findings, not whether the
-- self-test command was accepted. Bits 0-2 are the command/device failures.
local accepted = result.timedOut ~= true and exitCode ~= nil and exitCode >= 0 and exitCode % 8 == 0
if accepted then
selfTestLaunch.state = "starting"
selfTestLaunch.accepted_at = os.time()
selfTestLaunch.accepted_snapshot_epoch = number(snapshot and snapshot.generated_at_epoch, 0)
noctalia.notify(noctalia.tr("self_test.title"), noctalia.tr("self_test.started_background"))
local nonce = number(noctalia.state.get("refresh_nonce"), 0) + 1
noctalia.state.set("refresh_nonce", nonce)
else
selfTestLaunch.state = "failed"
selfTestLaunch.error = selfTestError(result)
noctalia.notifyError(noctalia.tr("self_test.title"), selfTestLaunch.error)
end
render()
end, 120000)
if not launched then
selfTestLaunch.state = "failed"
selfTestLaunch.error = noctalia.tr("self_test.launch_failed")
noctalia.notifyError(noctalia.tr("self_test.title"), selfTestLaunch.error)
render()
end
end
local function privilegedActionError(result)
if result.timedOut == true then
return noctalia.tr("privileged_action.timeout")
end
if tonumber(result.exitCode) == 126 then
return noctalia.tr("privileged_action.cancelled")
end
local detail = noctalia.string.trim(tostring(result.stderr or ""))
if detail == "" then detail = noctalia.string.trim(tostring(result.stdout or "")) end
if #detail > 240 then detail = detail:sub(1, 237) .. "..." end
return detail ~= "" and detail or noctalia.tr("privileged_action.command_failed")
end
local function runPrivilegedAction(command, details)
if command == nil or command == "" or (privilegedAction ~= nil and privilegedAction.state == "authorizing")
or (intervalApply ~= nil and intervalApply.state == "authorizing") then
return
end
privilegedAction = { state = "authorizing", scope = details.scope, kind = details.kind }
render()
local launched = noctalia.runAsync(command, function(result)
if result.timedOut ~= true and tonumber(result.exitCode) == 0 then
privilegedAction = nil
noctalia.notify(details.title, noctalia.tr("privileged_action.completed", { action = details.action }))
local nonce = number(noctalia.state.get("refresh_nonce"), 0) + 1
noctalia.state.set("refresh_nonce", nonce)
else
privilegedAction = {
state = "failed", scope = details.scope, kind = details.kind,
error = privilegedActionError(result),
}
noctalia.notifyError(details.title, noctalia.tr("privileged_action.failed", {
action = details.action, error = privilegedAction.error,
}))
end
render()
end, 120000)
if not launched then
privilegedAction = {
state = "failed", scope = details.scope, kind = details.kind,
error = noctalia.tr("privileged_action.launch_failed"),
}
noctalia.notifyError(details.title, noctalia.tr("privileged_action.failed", {
action = details.action, error = privilegedAction.error,
}))
render()
end
end
function onInstallCollectorClicked()
local command = snapshot and snapshot.system_collector and snapshot.system_collector.install_command or nil
runPrivilegedAction(command, {
scope = "collector", kind = "install", title = noctalia.tr("collector.title"),
action = noctalia.tr("collector.action_install"),
})
end
function onStartCollectorClicked()
local command = snapshot and snapshot.system_collector and snapshot.system_collector.enable_command or nil
runPrivilegedAction(command, {
scope = "collector", kind = "start", title = noctalia.tr("collector.title"),
action = noctalia.tr("collector.action_start"),
})
end
function onPauseCollectorClicked()
local command = snapshot and snapshot.system_collector and snapshot.system_collector.disable_command or nil
runPrivilegedAction(command, {
scope = "collector", kind = "pause", title = noctalia.tr("collector.title"),
action = noctalia.tr("collector.action_pause"),
})
end
function onApplyCollectorIntervalClicked()
if (intervalApply ~= nil and intervalApply.state == "authorizing")
or (privilegedAction ~= nil and privilegedAction.state == "authorizing") then return end
local command = snapshot and snapshot.system_collector and snapshot.system_collector.interval_command or nil
if command == nil or command == "" then return end
intervalApply = { state = "authorizing" }
render()
local launched = noctalia.runAsync(command, function(result)
if result.timedOut ~= true and tonumber(result.exitCode) == 0 then
intervalApply = nil
noctalia.notify(noctalia.tr("collector.title"), noctalia.tr("collector.interval_applied"))
local nonce = number(noctalia.state.get("refresh_nonce"), 0) + 1
noctalia.state.set("refresh_nonce", nonce)
else
local error
if result.timedOut == true then
error = noctalia.tr("collector.interval_timeout")
elseif tonumber(result.exitCode) == 126 then
error = noctalia.tr("collector.interval_cancelled")
else
local detail = noctalia.string.trim(tostring(result.stderr or ""))
if detail == "" then detail = noctalia.string.trim(tostring(result.stdout or "")) end
if #detail > 240 then detail = detail:sub(1, 237) .. "..." end
error = detail ~= "" and detail or noctalia.tr("collector.interval_command_failed")
end
intervalApply = { state = "failed", error = error }
noctalia.notifyError(noctalia.tr("collector.title"), error)
end
render()
end, 120000)
if not launched then
intervalApply = { state = "failed", error = noctalia.tr("collector.interval_launch_failed") }
noctalia.notifyError(noctalia.tr("collector.title"), intervalApply.error)
render()
end
end
function onToggleCollectorSettingsClicked()
showCollectorSettings = not showCollectorSettings
confirmUninstall = false
render()
end
function onOpenPluginSettingsClicked()
panel.close()
noctalia.runAsync("noctalia msg settings-open plugins", function(_result) end, 5000)
noctalia.notify(noctalia.tr("collector.settings_title"), noctalia.tr("collector.settings_opened"))
end
function onCopyCollectorCommandClicked()
local collector = snapshot and snapshot.system_collector or nil
local command = collector and collector.status == "stale" and collector.enable_command
or (collector and collector.install_command or nil)
if command ~= nil and noctalia.copyToClipboard(command, "text/plain") then
noctalia.notify(noctalia.tr("collector.title"), noctalia.tr("dependencies.copied"))
end
end
function onUninstallCollectorClicked()
if not confirmUninstall then
confirmUninstall = true
render()
return
end
local command = snapshot and snapshot.system_collector and snapshot.system_collector.uninstall_command or nil
confirmUninstall = false
runPrivilegedAction(command, {
scope = "collector", kind = "remove", title = noctalia.tr("collector.title"),
action = noctalia.tr("collector.action_remove"),
})
end
function onOpen(_context)
opened = true
render()
end
function onClose()
opened = false
pendingSelfTest = nil
confirmUninstall = false
showCollectorSettings = false
end
function onRefreshClicked()
local nonce = number(noctalia.state.get("refresh_nonce"), 0) + 1
noctalia.state.set("refresh_nonce", nonce)
end
function onInstallDependenciesClicked()
local dependencies = snapshot and snapshot.dependencies or nil
local command = dependencies and dependencies.install_command or nil
if command == nil or command == "" then
noctalia.notifyError(noctalia.tr("dependencies.title"), noctalia.tr("dependencies.manual_install"))
return
end
runPrivilegedAction(command, {
scope = "dependencies", kind = "install", title = noctalia.tr("dependencies.title"),
action = noctalia.tr("dependencies.action_install"),
})
end
function onCopyInstallCommandClicked()
local dependencies = snapshot and snapshot.dependencies or nil
local command = dependencies and dependencies.install_command or nil
if command == nil or command == "" then
return
end
if noctalia.copyToClipboard(command, "text/plain") then
noctalia.notify(noctalia.tr("dependencies.title"), noctalia.tr("dependencies.copied"))
end
end
function onCloseClicked()
panel.close()
end