--!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 toggleDriveAt local dismissAlertAt 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 issueIndex = index 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", onClick = function() dismissAlertAt(issueIndex) end }), })) 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"), onClick = function() toggleDriveAt(index) end, }), }), 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) toggleDriveAt = function(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 local function sendDismissRequest(request) dismissRequestNonce += 1 request.nonce = dismissRequestNonce noctalia.state.set("dismiss_alert_request", request) end dismissAlertAt = function(index) local issue = currentIssues[index] if issue ~= nil and issue.id ~= nil then sendDismissRequest({ id = tostring(issue.id) }) end 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