Add nilsonlinux/link-ip-monitor v1.0.0 (#320)

* Add nilsonlinux/link-ip-monitor v1.0.0

* Add nilsonlinux/link-ip-monitor v1.0.0

* Update plugin ID in README.md

* Rename project to link-ip-monitor and update ID

Updated the project name and ID in the README.

* Update plugin ID and command syntax in README
This commit is contained in:
Nilsonlinux
2026-08-09 13:50:32 -04:00
committed by GitHub
parent 42bea0452b
commit af32a36f1c
8 changed files with 991 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
# link-ip-monitor
link-ip-monitor pings a list of IPs, hostnames or links on an interval, tracks
each host's online/offline status and response time, and notifies when one
goes down or comes back up.
## Plugin
| Field | Value |
| --- | --- |
| ID | `nilsonlinux/link-ip-monitor` |
| Entries | Bar widget: `status`; panel: `panel`; service: `monitor` |
## Requirements
Uses the system `ping` binary (`iputils`, present on most Linux distros).
No other external dependency.
## Usage
Add the `status` widget to the bar: it turns green when every monitored
host is responding, red with a count badge when one or more are down, and
neutral when the list is empty.
Click the widget to open the panel. Use the `+` button in the panel header
to reveal the add-host form — accepts an IP (`8.8.8.8`), a hostname
(`example.com`) or a full link (scheme, port, path and query are stripped
automatically, keeping only the host to ping). An optional description can
be set alongside it.
Each row in the panel shows the label (or the host, if no label was set), a
status pill, the response time in ms while online, a drag handle to
reorder the list, and a trash button to remove that entry (asks for
confirmation). Right-click the widget, or use the refresh button in the
panel, to force an immediate check.
Open the panel directly with:
```sh
noctalia msg panel-toggle nilsonlinux/link-ip-monitor:panel
```
Force an immediate check via IPC:
```sh
noctalia msg plugin nilsonlinux/link-ip-monitor:monitor all refresh
```
The host list itself is not stored in plugin settings — it lives in the
service's own persisted state (`pluginDataDir()/state.json`) and is
managed entirely from the panel (add / remove / reorder), since the
plugin API has no way to write settings back from a script.
## Settings
| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `interval_seconds` | `int` | `30` | How often each host is checked. |
| `timeout_seconds` | `int` | `1` | How long to wait for a ping reply before treating it as a failure. |
| `notify_on_recovery` | `bool` | `true` | Send a notification when a host that was down responds again. |
| `glyph` (widget) | `glyph` | `activity` | Icon shown in the bar for the `status` widget. |
## Notes
Notification text and panel labels come from `translations/<locale>.json`
via `noctalia.tr()` (English and `pt-BR` included). `noctalia.notify()` /
`noctalia.notifyError()` accept only a title and body — the plugin API
exposes no urgency parameter, so the "host down" alert's appearance (the
red border from `notifyError`) can't be further customized from the
plugin. Response time (ms) is parsed from `ping`'s own text output, so it
depends on that output containing a `=<number> ms` pattern regardless of
the system's locale.
+359
View File
@@ -0,0 +1,359 @@
--!nonstrict
-- panel.luau
-- Window shown when clicking the bar widget: a form to add an IP,
-- hostname or link (with an optional description field below it), and
-- the list of monitored hosts — each with its label (if set), a status
-- pill, response time in ms (when online), a drag handle to reorder, and
-- a remove button (with confirmation).
local ips = {} -- { {ip=, label=, status=, latency_ms=}, ... }
local draftIp = ""
local draftLabel = ""
local draftKey = 0
local draftError = ""
local pendingDelete = nil
local showAddForm = false
local DRAG_TYPE = "link-ip-monitor-host"
local STATUS_COLORS = {
up = { fill = "#2e7d32", text = "#ffffff" },
down = { fill = "#c62828", text = "#ffffff" },
checking = { fill = "#5f6368", text = "#ffffff" },
}
local render
-- Accepts an IP, a plain hostname, or a link (http://..., https://...)
-- and returns just the host — scheme, port, path and query are dropped.
-- Same logic as service.luau (duplicated here just to give immediate
-- feedback in the field).
local function extractHost(raw)
local s = noctalia.string.trim(raw or "")
s = s:gsub("^%a[%w+.-]*://", "")
s = s:match("^([^/%?#]+)") or s
s = s:match("^([^:]+)") or s
return s
end
local function isValidIPv4(host)
local a, b, c, d = host:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$")
if not a then
return false
end
for _, n in ipairs({ a, b, c, d }) do
local num = tonumber(n)
if num == nil or num < 0 or num > 255 then
return false
end
end
return true
end
local function isValidHostname(host)
if host == "" or #host > 253 then
return false
end
local labelCount = 0
for label in host:gmatch("[^%.]+") do
labelCount = labelCount + 1
if #label == 0 or #label > 63 then
return false
end
if not label:match("^%w[%w%-]*%w$") and not label:match("^%w$") then
return false
end
end
return labelCount > 0
end
local function normalizeHost(raw)
local host = extractHost(raw)
if host == "" then
return nil
end
if isValidIPv4(host) or isValidHostname(host) then
return host
end
return nil
end
local STATUS_TR_KEY = {
up = "panel.status_online",
down = "panel.status_offline",
checking = "panel.status_checking",
}
local function pill(status)
local colors = STATUS_COLORS[status] or STATUS_COLORS.checking
local label = noctalia.tr(STATUS_TR_KEY[status] or "panel.status_checking")
return ui.row({
fill = colors.fill,
radius = 8,
paddingH = 8,
paddingV = 2,
align = "center",
justify = "center",
}, {
ui.label({ text = label, fontSize = 11, fontWeight = "bold", color = colors.text }),
})
end
-- Thin strip between rows (and before the first / after the last) that
-- accepts a dropped host, indicating where it will land.
local function insertionZone(index)
return ui.dropZone({
key = "gap-" .. index,
accepts = { DRAG_TYPE },
value = tostring(index),
onDrop = "onIpDropped",
height = 3,
radius = 4,
expandOnDrag = true,
hitSlop = 24,
})
end
function onIpDropped(payload, value)
local insertAt = tonumber(value)
if type(payload) ~= "string" or payload == "" or insertAt == nil then
return
end
noctalia.state.set("link_ip_monitor.cmd", { op = "reorder", ip = payload, index = insertAt })
end
local function hostRow(item)
local id = item.ip
local deleting = pendingDelete == id
local actions
if deleting then
actions = ui.row({ gap = 4, align = "center" }, {
ui.button({
glyph = "check",
variant = "destructive",
controlSize = "sm",
tooltip = noctalia.tr("panel.tooltip_confirm_remove"),
onClick = function()
noctalia.state.set("link_ip_monitor.cmd", { op = "remove", ip = id })
pendingDelete = nil
render()
end,
}),
ui.button({
glyph = "close",
variant = "ghost",
controlSize = "sm",
tooltip = noctalia.tr("panel.tooltip_cancel"),
onClick = function()
pendingDelete = nil
render()
end,
}),
})
else
actions = ui.button({
glyph = "trash",
variant = "ghost",
controlSize = "sm",
tooltip = noctalia.tr("panel.tooltip_remove"),
onClick = function()
pendingDelete = id
render()
end,
})
end
-- Left column: label (if set) in bold + host as a caption; without a
-- label, the host becomes the main text.
local infoChildren = {}
local hasLabel = item.label ~= nil and item.label ~= ""
if hasLabel then
table.insert(infoChildren, ui.label({ text = item.label, fontWeight = "bold", fontSize = 13 }))
table.insert(infoChildren, ui.label({ text = item.ip, color = "on_surface_variant", fontSize = 10 }))
else
table.insert(infoChildren, ui.label({ text = item.ip, fontWeight = "bold", fontSize = 13 }))
end
-- Right column: status pill + response time in ms (only while online
-- and when the measurement exists).
local statusChildren = { pill(item.status) }
if item.status == "up" and item.latency_ms ~= nil then
table.insert(statusChildren, ui.label({
text = tostring(math.floor(item.latency_ms + 0.5)) .. " ms",
fontSize = 10,
color = "on_surface_variant",
}))
end
return ui.row({
key = "ip-" .. id,
gap = 8,
padding = 10,
radius = 8,
fill = "surface_variant/0.35",
align = "center",
justify = "space_between",
}, {
ui.dragSource({
key = "grip-" .. id,
dragType = DRAG_TYPE,
payload = id,
previewAncestor = 1,
liftFromLayout = true,
width = 20,
height = 20,
align = "center",
justify = "center",
tooltip = noctalia.tr("panel.tooltip_drag"),
}, {
ui.glyph({ name = "menu-2", size = 14, color = "on_surface_variant" }),
}),
ui.column({ flexGrow = 1, gap = 1 }, infoChildren),
ui.column({ align = "end", gap = 2 }, statusChildren),
actions,
})
end
local function emptyState()
return ui.column({ gap = 10, align = "center", padding = 32, flexGrow = 1, justify = "center" }, {
ui.glyph({ name = "activity", size = 32, color = "outline" }),
ui.label({ text = noctalia.tr("panel.empty"), color = "outline" }),
})
end
render = function()
local body
if #ips == 0 then
body = emptyState()
else
local rows = {}
for i, item in ipairs(ips) do
table.insert(rows, insertionZone(i))
table.insert(rows, hostRow(item))
end
table.insert(rows, insertionZone(#ips + 1))
body = ui.scroll({ gap = 6, flexGrow = 1 }, rows)
end
local column = {}
table.insert(column, ui.row({ align = "center", justify = "space_between" }, {
ui.label({ text = noctalia.tr("title"), fontSize = 16, fontWeight = "bold", flexGrow = 1 }),
ui.button({
glyph = showAddForm and "minus" or "plus",
variant = showAddForm and "primary" or "ghost",
controlSize = "sm",
tooltip = showAddForm and noctalia.tr("panel.tooltip_close_form") or noctalia.tr("panel.tooltip_add_host"),
onClick = function()
showAddForm = not showAddForm
if not showAddForm then
draftIp = ""
draftLabel = ""
draftError = ""
draftKey += 1
end
render()
end,
}),
ui.button({
glyph = "refresh",
variant = "ghost",
controlSize = "sm",
tooltip = noctalia.tr("panel.tooltip_check_now"),
onClick = function() noctalia.state.set("link_ip_monitor.cmd", { op = "refresh" }) end,
}),
ui.button({
glyph = "close",
variant = "ghost",
controlSize = "sm",
onClick = function() panel.close() end,
}),
}))
if showAddForm then
table.insert(column, ui.row({
gap = 6,
align = "center",
padding = 8,
radius = 8,
fill = "surface_variant/0.35",
}, {
ui.column({ flexGrow = 1, gap = 6 }, {
ui.input({
key = "add-ip-" .. draftKey,
value = draftIp,
placeholder = noctalia.tr("panel.add_placeholder"),
onChange = function(value) draftIp = value; draftError = "" end,
onSubmit = "onAdd",
}),
ui.input({
key = "add-label-" .. draftKey,
value = draftLabel,
placeholder = noctalia.tr("panel.add_label_placeholder"),
onChange = function(value) draftLabel = value end,
onSubmit = "onAdd",
}),
(draftError ~= "" and ui.label({ text = draftError, color = "#e05252", fontSize = 11 }) or nil),
}),
ui.button({ glyph = "plus", variant = "primary", controlSize = "sm", tooltip = noctalia.tr("panel.tooltip_add"), onClick = "onAdd" }),
}))
end
table.insert(column, body)
panel.render(ui.column({ gap = 8, padding = 8, flexGrow = 1 }, column))
end
function onAdd()
local host = normalizeHost(draftIp)
if noctalia.string.trim(draftIp) == "" then
return
end
if host == nil then
draftError = noctalia.tr("panel.invalid_format")
showAddForm = true
render()
return
end
local label = noctalia.string.trim(draftLabel)
draftIp = ""
draftLabel = ""
draftKey += 1
draftError = ""
showAddForm = false
noctalia.state.set("link_ip_monitor.cmd", { op = "add", ip = host, label = label })
render()
end
function onOpen(_context)
ips = noctalia.state.get("link_ip_monitor.statuses") or {}
pendingDelete = nil
draftIp = ""
draftLabel = ""
draftError = ""
draftKey += 1
showAddForm = false
render()
end
noctalia.state.watch("link_ip_monitor.statuses", function(value)
ips = (type(value) == "table") and value or {}
if pendingDelete ~= nil then
local stillThere = false
for _, item in ipairs(ips) do
if item.ip == pendingDelete then
stillThere = true
break
end
end
if not stillThere then
pendingDelete = nil
end
end
render()
end)
+76
View File
@@ -0,0 +1,76 @@
id = "nilsonlinux/link-ip-monitor"
name = "Link/IP Monitor"
version = "1.0.0"
plugin_api = 14
author = "Nilsonlinux"
license = "MIT"
icon = "activity"
description = "It pings a list of IPs, hosts, or links at intervals and notifies you when one goes down (or comes back up)."
tags = ["network", "indicator", "bar", "panel", "service"]
dependencies = ["ping"]
# ---------------------------------------------------------------------------
# Plugin settings: shared by ALL entries (service + widget). Edited in
# Settings -> Plugins.
# ---------------------------------------------------------------------------
[[setting]]
key = "interval_seconds"
type = "int"
label_key = "settings.interval_seconds.label"
description_key = "settings.interval_seconds.description"
default = 30
min = 5
max = 3600
[[setting]]
key = "timeout_seconds"
type = "int"
label_key = "settings.timeout_seconds.label"
description_key = "settings.timeout_seconds.description"
default = 1
min = 1
max = 10
[[setting]]
key = "notify_on_recovery"
type = "bool"
label_key = "settings.notify_on_recovery.label"
description_key = "settings.notify_on_recovery.description"
default = true
# ---------------------------------------------------------------------------
# Service: runs in the background, pings hosts, and publishes state
# ---------------------------------------------------------------------------
[[service]]
id = "monitor"
entry = "service.luau"
# ---------------------------------------------------------------------------
# Bar widget: icon with a badge for the number of hosts that are down
# ---------------------------------------------------------------------------
[[widget]]
id = "status"
entry = "widget.luau"
[[widget.setting]]
key = "glyph"
type = "glyph"
label_key = "settings.glyph.label"
description_key = "settings.glyph.description"
default = "activity"
# ---------------------------------------------------------------------------
# Panel: list of hosts with a status pill, opened by clicking the widget
# ---------------------------------------------------------------------------
[[panel]]
id = "panel"
entry = "panel.luau"
width = 320
height = 400
placement = "attached"
position = "auto"
open_near_click = true
+307
View File
@@ -0,0 +1,307 @@
--!nonstrict
-- service.luau
-- Headless service: keeps the host list (IP, hostname or link — each with
-- an optional label), pings every entry, measures latency, and notifies
-- on the up -> down transition (and, when enabled, down -> up).
--
-- Persistence: pluginDataDir()/state.json ({ ips = { {ip=, label=}, ... } })
-- State: link_ip_monitor.statuses = { {ip=, label=, status=, latency_ms=}, ... }
-- link_ip_monitor.down_count = number
-- Panel commands: ip_monitor.cmd =
-- { op = "add"|"remove"|"refresh"|"reorder", ip = "...", label? = "...", index? = N }
local ipList = {} -- { {ip=, label=}, ... } — "ip" here is the already-normalized host (no scheme/port/path)
local hostState = {} -- host -> true (up) / false (down) / nil (not checked yet)
local hostLatency = {} -- host -> latency in ms (number) / nil
local dataPath = nil
local function trim(s)
return noctalia.string.trim(s or "")
end
-- Validates IPv4 in N.N.N.N form, each octet between 0 and 255.
local function isValidIPv4(host)
local a, b, c, d = host:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$")
if not a then
return false
end
for _, n in ipairs({ a, b, c, d }) do
local num = tonumber(n)
if num == nil or num < 0 or num > 255 then
return false
end
end
return true
end
-- Validates a hostname (dot-separated labels, letters/digits/hyphen, no
-- leading/trailing hyphen per label).
local function isValidHostname(host)
if host == "" or #host > 253 then
return false
end
local labelCount = 0
for label in host:gmatch("[^%.]+") do
labelCount = labelCount + 1
if #label == 0 or #label > 63 then
return false
end
if not label:match("^%w[%w%-]*%w$") and not label:match("^%w$") then
return false
end
end
return labelCount > 0
end
-- Accepts an IP, a plain hostname, or a link (http://..., https://...)
-- and returns just the host — scheme, port, path and query are dropped.
-- That clean host is what actually becomes the ping target (ping doesn't
-- understand a full URL).
local function extractHost(raw)
local s = trim(raw)
s = s:gsub("^%a[%w+.-]*://", "") -- strip a scheme like http:// or https://
s = s:match("^([^/%?#]+)") or s -- cut path/query/fragment
s = s:match("^([^:]+)") or s -- cut an explicit port (host:port)
return s
end
local function normalizeHost(raw)
local host = extractHost(raw)
if host == "" then
return nil
end
if isValidIPv4(host) or isValidHostname(host) then
return host
end
return nil
end
local function indexOfIp(ip)
for i, entry in ipairs(ipList) do
if entry.ip == ip then
return i
end
end
return nil
end
local function loadState()
local dir = noctalia.pluginDataDir()
if dir == nil then
return
end
dataPath = dir .. "/state.json"
local raw = noctalia.readFile(dataPath)
if raw ~= nil then
local ok, decoded = pcall(noctalia.json.decode, raw)
if ok and type(decoded) == "table" and type(decoded.ips) == "table" then
for _, entry in ipairs(decoded.ips) do
if type(entry) == "table" and type(entry.ip) == "string" then
table.insert(ipList, { ip = entry.ip, label = trim(entry.label) })
elseif type(entry) == "string" then
-- backward compatibility with the old format (list of plain strings, no label)
table.insert(ipList, { ip = entry, label = "" })
end
end
end
end
-- no file yet (first run): ipList stays empty until the user adds
-- something from the panel.
end
local function saveState()
if dataPath == nil then
return
end
local ok, encoded = pcall(noctalia.json.encode, { ips = ipList })
if ok and type(encoded) == "string" then
pcall(noctalia.writeFile, dataPath, encoded)
end
end
local function publishState()
local statuses = {}
local downCount = 0
for _, entry in ipairs(ipList) do
local up = hostState[entry.ip]
local status
if up == true then
status = "up"
elseif up == false then
status = "down"
downCount = downCount + 1
else
status = "checking"
end
table.insert(statuses, {
ip = entry.ip,
label = entry.label,
status = status,
latency_ms = (status == "up") and hostLatency[entry.ip] or nil,
})
end
noctalia.state.set("link_ip_monitor.statuses", statuses)
noctalia.state.set("link_ip_monitor.down_count", downCount)
end
local function checkHost(entry)
local ip = entry.ip
local timeout = noctalia.getConfig("timeout_seconds") or 1
-- -c 1: send 1 packet; -W: timeout in seconds (iputils/Linux). ping
-- resolves a hostname via DNS normally, so a host like "example.com"
-- (extracted from a link) works just like an IP.
local cmd = string.format("ping -c 1 -W %d %s", timeout, ip)
noctalia.runAsync(cmd, function(res)
local success = res.exitCode == 0
local latency = nil
if success and type(res.stdout) == "string" then
-- Ignores the word before "=" or "<" (time=, tempo=, temps=...) —
-- ping's output changes with the system locale. Only looks for the
-- number that comes right before "ms".
local ms = res.stdout:match("[=<]%s*(%d+%.?%d*)%s*ms")
latency = ms and tonumber(ms) or nil
end
local wasUp = hostState[ip]
hostState[ip] = success
hostLatency[ip] = latency
if wasUp ~= success then
local display = (entry.label ~= "" and entry.label) or ip
if success == false then
noctalia.notifyError(noctalia.tr("title"), noctalia.tr("notify.host_down", { host = display }))
elseif wasUp == false and success == true and noctalia.getConfig("notify_on_recovery") then
noctalia.notify(noctalia.tr("title"), noctalia.tr("notify.host_up", { host = display }))
end
end
publishState()
end, (timeout + 2) * 1000)
end
local function checkAll()
for _, entry in ipairs(ipList) do
checkHost(entry)
end
end
local function applyInterval()
local seconds = noctalia.getConfig("interval_seconds") or 30
noctalia.setUpdateInterval(seconds * 1000)
end
-- Returns ok, reason ("invalid" | "duplicate") on failure.
local function addIp(raw, label)
local host = normalizeHost(raw)
if host == nil then
return false, "invalid"
end
if indexOfIp(host) ~= nil then
return false, "duplicate"
end
local entry = { ip = host, label = trim(label) }
table.insert(ipList, entry)
saveState()
publishState()
checkHost(entry)
return true
end
local function removeIp(ip)
local idx = indexOfIp(ip)
if idx ~= nil then
table.remove(ipList, idx)
end
hostState[ip] = nil
hostLatency[ip] = nil
saveState()
publishState()
end
-- Moves `ip` to 1-based position `index` in the final list (drag and drop
-- in the panel) — same logic as the official world_clock's reorderZone().
local function reorderIp(ip, index)
local fromIdx = indexOfIp(ip)
if fromIdx == nil then
return false
end
local insertAt = tonumber(index)
if insertAt == nil then
return false
end
local entry = table.remove(ipList, fromIdx)
if fromIdx < insertAt then
insertAt -= 1
end
insertAt = math.max(1, math.min(insertAt, #ipList + 1))
table.insert(ipList, insertAt, entry)
saveState()
publishState()
return true
end
-- ---------------------------------------------------------------------------
-- Commands coming from the panel, via noctalia.state (same pattern as the
-- official world_clock: the panel sets "ip_monitor.cmd", the service
-- watches it, processes it, and clears it back to nil).
-- ---------------------------------------------------------------------------
noctalia.state.watch("link_ip_monitor.cmd", function(cmd)
if type(cmd) ~= "table" then
return
end
local op = cmd.op
if op == "add" then
local ok, reason = addIp(cmd.ip, cmd.label)
if not ok then
local display = tostring(cmd.ip or "")
if reason == "invalid" then
noctalia.notifyError(noctalia.tr("title"), noctalia.tr("notify.invalid_host", { host = display }))
elseif reason == "duplicate" then
noctalia.notifyError(noctalia.tr("title"), noctalia.tr("notify.duplicate_host", { host = display }))
end
end
elseif op == "remove" then
removeIp(cmd.ip)
elseif op == "reorder" then
reorderIp(cmd.ip, cmd.index)
elseif op == "refresh" then
checkAll()
end
noctalia.state.set("link_ip_monitor.cmd", nil)
end)
-- ---------------------------------------------------------------------------
-- Lifecycle
-- ---------------------------------------------------------------------------
loadState()
applyInterval()
publishState()
checkAll()
function update()
checkAll()
end
function onConfigChanged()
applyInterval()
end
-- Still works via CLI too, if needed:
-- noctalia msg plugin nilsonlinux/link-ip-monitor:monitor all refresh
function onIpc(event, payload)
if event == "refresh" then
checkAll()
elseif event == "add" and payload then
addIp(payload, "")
elseif event == "remove" and payload then
removeIp(payload)
end
end
Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

+49
View File
@@ -0,0 +1,49 @@
{
"title": "Link/IP Monitor",
"settings": {
"interval_seconds": {
"label": "Check interval (s)",
"description": "How often each host is checked."
},
"timeout_seconds": {
"label": "Ping timeout (s)",
"description": "How long to wait for a reply before treating it as a failure."
},
"notify_on_recovery": {
"label": "Notify when a host comes back",
"description": "Send a notification when a host that was down responds again."
},
"glyph": {
"label": "Bar icon",
"description": "Icon shown in the bar for this widget."
}
},
"notify": {
"host_down": "{host} is unreachable",
"host_up": "{host} is responding again",
"invalid_host": "{host} is not a valid IP, host or link",
"duplicate_host": "{host} is already in the list"
},
"panel": {
"empty": "No hosts configured yet",
"add_placeholder": "IP, host or link",
"add_label_placeholder": "Description (optional) — e.g. Router, Server",
"invalid_format": "Invalid — enter an IP (0.0.0.0), a host or a link",
"status_online": "Online",
"status_offline": "Offline",
"status_checking": "Checking…",
"tooltip_check_now": "Check now",
"tooltip_add": "Add",
"tooltip_remove": "Remove",
"tooltip_confirm_remove": "Confirm removal",
"tooltip_cancel": "Cancel",
"tooltip_drag": "Drag to reorder",
"tooltip_close_form": "Close",
"tooltip_add_host": "Add host"
},
"widget": {
"tooltip_all_up": "All monitored hosts are responding",
"tooltip_down_prefix": "Down:",
"checking_now": "Checking now…"
}
}
+49
View File
@@ -0,0 +1,49 @@
{
"title": "Link/IP Monitor",
"settings": {
"interval_seconds": {
"label": "Intervalo entre checagens (s)",
"description": "Com que frequência cada host é checado."
},
"timeout_seconds": {
"label": "Timeout do ping (s)",
"description": "Quanto tempo esperar por resposta antes de considerar falha."
},
"notify_on_recovery": {
"label": "Notificar quando o host volta",
"description": "Envia uma notificação quando um host que estava fora do ar volta a responder."
},
"glyph": {
"label": "Ícone da barra",
"description": "Ícone mostrado na barra para este widget."
}
},
"notify": {
"host_down": "{host} está inacessível",
"host_up": "{host} voltou a responder",
"invalid_host": "{host} não é um IP, host ou link válido",
"duplicate_host": "{host} já está na lista"
},
"panel": {
"empty": "Nenhum host configurado ainda",
"add_placeholder": "IP, host ou link",
"add_label_placeholder": "Descrição (opcional) — ex: Roteador, Servidor",
"invalid_format": "Inválido — informe um IP (0.0.0.0), um host ou um link",
"status_online": "Online",
"status_offline": "Offline",
"status_checking": "Checando…",
"tooltip_check_now": "Checar agora",
"tooltip_add": "Adicionar",
"tooltip_remove": "Remover",
"tooltip_confirm_remove": "Confirmar remoção",
"tooltip_cancel": "Cancelar",
"tooltip_drag": "Arraste para reordenar",
"tooltip_close_form": "Fechar",
"tooltip_add_host": "Adicionar host"
},
"widget": {
"tooltip_all_up": "Todos os hosts monitorados estão respondendo",
"tooltip_down_prefix": "Fora do ar:",
"checking_now": "Checando agora…"
}
}
+87
View File
@@ -0,0 +1,87 @@
--!nonstrict
-- widget.luau
-- Bar widget: configurable icon (setting "glyph"); turns green when
-- everything responds, red with a count badge when something is down.
-- Click opens the panel with the full list.
local downCount = 0
local statuses = {}
local COLOR_DOWN = "#e05252"
local COLOR_UP = "#2e7d32"
local function downList()
local list = {}
for _, s in ipairs(statuses) do
if s.status == "down" then
local text = (s.label ~= nil and s.label ~= "") and (s.label .. " (" .. s.ip .. ")") or s.ip
table.insert(list, text)
end
end
return list
end
local function render()
local children = {}
local glyphName = noctalia.getConfig("glyph") or "activity"
local hasHosts = #statuses > 0
if downCount > 0 then
table.insert(children, ui.row({
fill = COLOR_DOWN,
radius = 8,
paddingH = 5,
paddingV = 1,
align = "center",
justify = "center",
}, {
ui.label({ text = tostring(downCount), fontSize = 10, fontWeight = "bold", color = "#ffffff" }),
}))
table.insert(children, ui.glyph({ name = glyphName, size = 14, color = COLOR_DOWN }))
elseif hasHosts then
-- everything responding: green icon, no badge
table.insert(children, ui.glyph({ name = glyphName, size = 14, color = COLOR_UP }))
else
-- no hosts configured yet: neutral color (neither green nor red)
table.insert(children, ui.glyph({ name = glyphName, size = 14 }))
end
local container = barWidget.isVertical() and ui.column or ui.row
barWidget.render(container({ gap = 4, align = "center" }, children))
local down = downList()
if #down > 0 then
barWidget.setTooltip(noctalia.tr("widget.tooltip_down_prefix") .. "\n" .. table.concat(down, "\n"))
else
barWidget.setTooltip(noctalia.tr("widget.tooltip_all_up"))
end
end
noctalia.state.watch("link_ip_monitor.down_count", function(value)
downCount = value or 0
render()
end)
noctalia.state.watch("link_ip_monitor.statuses", function(value)
statuses = (type(value) == "table") and value or {}
render()
end)
render()
function update()
noctalia.setUpdateInterval(5000) -- just keeps the widget "alive"; the real data comes from state
end
function onConfigChanged()
render() -- pick up the new icon when the user changes it in the widget settings
end
function onClick()
noctalia.togglePanel("nilsonlinux/link-ip-monitor:panel")
end
function onRightClick()
noctalia.state.set("link_ip_monitor.cmd", { op = "refresh" })
noctalia.notify(noctalia.tr("title"), noctalia.tr("widget.checking_now"))
end