Files
community-plugins/opnsense/launcher.luau
T
Dave HammerandGitHub 38d6255b98 Add davemhammer/opnsense (#315)
OPNsense health, interfaces, gateways, rules, logs, and service control.
2026-08-09 10:24:22 -04:00

293 lines
10 KiB
Luau

--!nonstrict
-- /opn launcher for OPNsense.
local STATE_KEY = "opn_snapshot"
local COMMAND_KEY = "opn_command"
local PANEL_ID = "davemhammer/opnsense:manager"
local MAX_ROWS = 40
local snapshot = noctalia.state.get(STATE_KEY) or {
available = false,
configured = false,
loading = true,
widgets = {},
interfaces = {},
gateways = {},
services = {},
issueCount = 0,
host = "",
error = "",
}
noctalia.state.watch(STATE_KEY, function(value)
if type(value) == "table" then
snapshot = value
end
end)
local function trim(s)
return noctalia.string.trim(tostring(s or ""))
end
local function lower(s)
return string.lower(tostring(s or ""))
end
local function send(action, values)
local command = { action = action, requestId = "launcher-" .. tostring(os.time()) }
if type(values) == "table" then
for k, v in pairs(values) do command[k] = v end
end
noctalia.state.set(COMMAND_KEY, command)
end
local function scoreText(filter, ...)
if filter == "" then return 1 end
local best = nil
for i = 1, select("#", ...) do
local text = tostring(select(i, ...) or "")
if text ~= "" then
local s = noctalia.fuzzyScore(filter, text)
if s ~= nil and (best == nil or s > best) then best = s end
if best == nil and lower(text):find(lower(filter), 1, true) then best = 0.5 end
end
end
return best
end
local function statusRow(title, subtitle, glyph)
return { id = "", title = title, subtitle = subtitle, glyph = glyph or "shield" }
end
local function topCategories()
return {
{ id = "cat:status", title = noctalia.tr("launcher.cat.status"), subtitle = noctalia.tr("launcher.cat.status-sub"), glyph = "heart-rate-monitor", score = 100 },
{ id = "cat:interfaces", title = noctalia.tr("launcher.cat.interfaces"), subtitle = noctalia.tr("launcher.cat.interfaces-sub"), glyph = "network", score = 90 },
{ id = "cat:gateways", title = noctalia.tr("launcher.cat.gateways"), subtitle = noctalia.tr("launcher.cat.gateways-sub"), glyph = "router", score = 85 },
{ id = "cat:services", title = noctalia.tr("launcher.cat.services"), subtitle = noctalia.tr("launcher.cat.services-sub"), glyph = "settings", score = 80 },
{ id = "cat:rules", title = noctalia.tr("launcher.cat.rules"), subtitle = noctalia.tr("launcher.cat.rules-sub"), glyph = "list-check", score = 75 },
{ id = "cat:logs", title = noctalia.tr("launcher.cat.logs"), subtitle = noctalia.tr("launcher.cat.logs-sub"), glyph = "file-text", score = 72 },
{ id = "act:panel", title = noctalia.tr("launcher.cat.panel"), subtitle = noctalia.tr("launcher.cat.panel-sub"), glyph = "layout-dashboard", score = 70 },
{ id = "act:ui", title = noctalia.tr("launcher.cat.ui"), subtitle = noctalia.tr("launcher.cat.ui-sub"), glyph = "external-link", score = 60 },
{ id = "act:refresh", title = noctalia.tr("launcher.cat.refresh"), subtitle = noctalia.tr("launcher.cat.refresh-sub"), glyph = "refresh", score = 50 },
}
end
local function listStatus(filter)
local rows = {}
for _, w in ipairs(snapshot.widgets or {}) do
local s = scoreText(filter, w.name, w.status, w.message)
if s ~= nil then
table.insert(rows, {
id = "st:" .. w.id,
title = w.name,
subtitle = w.status .. (w.message ~= "" and (" · " .. w.message) or ""),
glyph = w.ok and "circle-check" or "circle-x",
score = s,
})
end
end
if #rows == 0 then rows[1] = statusRow(noctalia.tr("launcher.no-matches"), filter, "search") end
return rows
end
local function listIfaces(filter)
local rows = {}
for _, i in ipairs(snapshot.interfaces or {}) do
local s = scoreText(filter, i.name, i.description, i.status, i.ipv4)
if s ~= nil then
table.insert(rows, {
id = "if:" .. i.id,
title = i.name,
subtitle = `{i.status} · {i.ipv4}`,
glyph = "network",
score = s,
})
end
end
if #rows == 0 then rows[1] = statusRow(noctalia.tr("launcher.no-matches"), filter, "search") end
return rows
end
local function listGateways(filter)
local rows = {}
for _, g in ipairs(snapshot.gateways or {}) do
local s = scoreText(filter, g.name, g.status, g.address)
if s ~= nil then
table.insert(rows, {
id = "gw:" .. g.id,
title = g.name,
subtitle = `{g.status} · {g.address}`,
glyph = "router",
score = s,
})
end
end
if #rows == 0 then rows[1] = statusRow(noctalia.tr("launcher.no-matches"), filter, "search") end
return rows
end
local function serviceActions(name)
return {
{ id = "svcact:restart:" .. name, title = noctalia.tr("launcher.action.restart"), subtitle = name, glyph = "refresh", score = 100 },
{ id = "svcact:start:" .. name, title = noctalia.tr("launcher.action.start"), subtitle = name, glyph = "player-play", score = 90 },
{ id = "svcact:stop:" .. name, title = noctalia.tr("launcher.action.stop"), subtitle = name, glyph = "player-stop", score = 80 },
{ id = "svcact:copy:" .. name, title = noctalia.tr("launcher.action.copy"), subtitle = name, glyph = "copy", score = 70 },
}
end
local function listServices(filter)
local rows = {}
for _, s in ipairs(snapshot.services or {}) do
local sc = scoreText(filter, s.name, s.status, s.description)
if sc ~= nil then
table.insert(rows, {
id = "svc:" .. s.id,
title = s.name,
subtitle = s.status .. (s.description ~= "" and (" · " .. s.description) or ""),
glyph = s.running and "player-play" or "player-stop",
score = sc,
})
end
end
table.sort(rows, function(a, b) return (a.score or 0) > (b.score or 0) end)
while #rows > MAX_ROWS do table.remove(rows) end
if #rows == 0 then rows[1] = statusRow(noctalia.tr("launcher.no-matches"), filter, "search") end
return rows
end
local function findService(name)
name = trim(name)
for _, s in ipairs(snapshot.services or {}) do
if s.name == name or s.id == name then return s end
end
local hits = {}
local q = lower(name)
for _, s in ipairs(snapshot.services or {}) do
if lower(s.name):find(q, 1, true) then table.insert(hits, s) end
end
if #hits == 1 then return hits[1] end
return nil
end
function onQuery(query)
if not snapshot.configured then
launcher.setResults(query, {
statusRow(noctalia.tr("panel.not_configured"), "", "settings"),
{ id = "act:panel", title = noctalia.tr("launcher.cat.panel"), subtitle = "Configure in plugin settings", glyph = "layout-dashboard" },
})
return
end
if snapshot.loading and not snapshot.available then
send("refresh")
launcher.setResults(query, { statusRow(noctalia.tr("launcher.loading"), snapshot.host, "loader") })
return
end
if not snapshot.available then
launcher.setResults(query, {
statusRow(noctalia.tr("launcher.unavailable"), snapshot.error or "", "cloud-off"),
{ id = "act:refresh", title = noctalia.tr("launcher.cat.refresh"), subtitle = "", glyph = "refresh" },
})
return
end
local text = trim(query)
if text == "" then
launcher.setResults(query, topCategories())
return
end
local tokens = {}
for t in text:gmatch("%S+") do table.insert(tokens, t) end
local head = lower(tokens[1] or "")
local rest = table.concat(tokens, " ", 2)
local function is(name, aliases)
if head == name then return true end
for _, a in ipairs(aliases) do if head == a then return true end end
return false
end
if is("status", { "st", "health" }) then
launcher.setResults(query, listStatus(rest))
return
end
if is("interfaces", { "iface", "if", "int" }) then
launcher.setResults(query, listIfaces(rest))
return
end
if is("gateways", { "gw", "gateway" }) then
launcher.setResults(query, listGateways(rest))
return
end
if is("services", { "svc", "service", "s" }) then
local svc = findService(rest)
if svc and (lower(rest) == lower(svc.name) or rest:find(svc.name, 1, true)) and rest ~= "" then
if lower(rest) == lower(svc.name) then
launcher.setResults(query, serviceActions(svc.name))
return
end
end
launcher.setResults(query, listServices(rest))
return
end
local rows = {}
for _, row in ipairs(topCategories()) do
local s = scoreText(text, row.title, row.id)
if s ~= nil then
row.score = s
table.insert(rows, row)
end
end
for _, r in ipairs(listServices(text)) do
if r.id ~= "" then table.insert(rows, r) end
end
if #rows == 0 then rows[1] = statusRow(noctalia.tr("launcher.no-matches"), text, "search") end
launcher.setResults(query, rows)
end
function onActivate(id)
if id == nil or id == "" then return end
if id == "cat:status" then launcher.setQuery("status "); return end
if id == "cat:interfaces" then launcher.setQuery("interfaces "); return end
if id == "cat:gateways" then launcher.setQuery("gateways "); return end
if id == "cat:services" then launcher.setQuery("services "); return end
if id == "cat:rules" then noctalia.togglePanel(PANEL_ID); return end
if id == "cat:logs" then noctalia.togglePanel(PANEL_ID); return end
if id == "act:panel" then noctalia.togglePanel(PANEL_ID); return end
if id == "act:ui" then send("open_ui"); return end
if id == "act:refresh" then
send("refresh")
noctalia.notify(noctalia.tr("title"), noctalia.tr("widget.refresh_requested"))
return
end
local svc = id:match("^svc:(.+)$")
if svc then
launcher.setQuery("services " .. svc .. " ")
return
end
local act, name = id:match("^svcact:([%w]+):(.+)$")
if act and name then
if act == "restart" then send("restart_service", { name = name })
elseif act == "start" then send("start_service", { name = name })
elseif act == "stop" then send("stop_service", { name = name })
elseif act == "copy" then send("copy", { name = name })
end
return
end
local st = id:match("^st:(.+)$")
if st then send("copy", { name = st }); return end
local iface = id:match("^if:(.+)$")
if iface then send("copy", { name = iface }); return end
local gw = id:match("^gw:(.+)$")
if gw then send("copy", { name = gw }); return end
end