fix(tools): keep the user on the Tools tab and show the result there

Running any tool jumped to the Tasks tab and left the user stranded there. That
is right for an install — long, log-heavy, worth watching — and wrong for a
tool, which is a short admin action whose answer is one line. Worse, half of
these are only meaningful back on Tools: List Users opens a modal over that tab,
and Create User Account returns a generated password that was being buried in a
log the user then had to go read.

Tools now stay put. On completion the tool's own outcome lines — the
isSuccessful/isError/isNotice output, ANSI stripped and framework boilerplate
filtered — are shown in a small result modal, with a View log button for
anything needing the full detail. list_users is left alone because the existing
account-list modal is already a better result view.

Also stops generate_arrays.sh walking scripts/dev. That directory is
`export-ignore`d, so it exists in a working clone but never in a shipped
install; generating a files_dev.sh entry from it wrote a reference into
files_source.sh that no install could satisfy, and the loader treats a missing
array file as a broken installation — every libreportal command stopped with
"files_dev.sh is missing from your LibrePortal Installation". Excluded alongside
unused/, system/ and release/. Regenerating also picked up scripts/validation,
which had never had an array file.

And Matrix's account listing prints its aligned line from python rather than
re-splitting the marker line in bash: TAB is IFS whitespace, so an empty display
name collapsed into the previous delimiter and shifted every later column.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-19 00:25:19 +01:00
parent e14e295f3f
commit 7aed9102c4
14 changed files with 262 additions and 59 deletions

View File

@ -5,7 +5,6 @@
# APP_NAME = name of application for use in scripts
# HOST_INSTALL = true means apt + systemd install on the host, not Docker
# HOST_PACKAGE = dpkg package name; drives the "installed" badge
# HOST_SERVICE = primary systemd unit; stop/restart actions hit this
# HOST_SERVICES = all units; feeds the Services + Logs tabs
# HOST_LOG_FILES = <unit>|<path>,... mapping for the log viewer
# BACKUP = include in backup operations
@ -16,7 +15,6 @@
CFG_CROWDSEC_APP_NAME=crowdsec
CFG_CROWDSEC_HOST_INSTALL=true
CFG_CROWDSEC_HOST_PACKAGE=crowdsec
CFG_CROWDSEC_HOST_SERVICE=crowdsec
CFG_CROWDSEC_HOST_SERVICES=crowdsec.service,crowdsec-firewall-bouncer.service
CFG_CROWDSEC_HOST_LOG_FILES="crowdsec.service|/var/log/crowdsec.log,crowdsec-firewall-bouncer.service|/var/log/crowdsec-firewall-bouncer.log"
CFG_CROWDSEC_BACKUP=true
@ -62,9 +60,7 @@ CFG_CROWDSEC_ACTIONS="configure|install|restart|shutdown|uninstall|tools"
# ADVANCED
# =============================================================================
# LAPI_HOST = LAPI bind address; 0.0.0.0 so Traefik can reach via host.docker.internal
# BOUNCER_NAME_TRAEFIK = bouncer name registered with cscli bouncers add
# TRAEFIK_LAPI_KEY = auto-generated by installCrowdsec; use the rotate Tools action to change
# TRAEFIK_LAPI_KEY = auto-generated on install; use the "Rotate Traefik Bouncer Key" tool to replace it (editing this value does not re-register the bouncer)
#
CFG_CROWDSEC_LAPI_HOST=0.0.0.0:8080
CFG_CROWDSEC_BOUNCER_NAME_TRAEFIK=traefik-bouncer
CFG_CROWDSEC_TRAEFIK_LAPI_KEY=

View File

@ -119,12 +119,13 @@ installCrowdsecHost()
# via host.docker.internal:host-gateway. The bouncer API key is
# required (HTTP 401 without it), so internet exposure is gated.
# External access on 8080 should still be blocked at UFW.
local lapi_host="${CFG_CROWDSEC_LAPI_HOST:-0.0.0.0:8080}"
local bind_result
bind_result=$(runCrowdsec bind-lapi 2>&1)
bind_result=$(runCrowdsec bind-lapi "$lapi_host" 2>&1)
if [[ "$bind_result" == "ALREADY_BOUND" ]]; then
isNotice "LAPI already bound to 0.0.0.0:8080 — skipping."
isNotice "LAPI already bound to ${lapi_host} — skipping."
else
checkSuccess "LAPI bound to 0.0.0.0:8080"
checkSuccess "LAPI bound to ${lapi_host}"
runCrowdsec services restart
checkSuccess "CrowdSec restarted"
fi
@ -203,9 +204,9 @@ installCrowdsecHost()
bouncer_key=$(tr -d '\r\n' < "$key_file")
[[ -n "$bouncer_key" ]] \
&& isNotice "Config had no bouncer key — recovering it from $key_file." \
|| isNotice "$key_file is empty — cannot recover the bouncer key. Rotate it to get a new one."
|| isNotice "$key_file is empty — cannot recover the bouncer key. Run the \"Rotate Traefik Bouncer Key\" tool."
else
isNotice "Config has no bouncer key and $key_file is not readable — rotate the bouncer to issue a new one."
isNotice "Config has no bouncer key and $key_file is not readable — run the \"Rotate Traefik Bouncer Key\" tool to issue a new one."
fi
fi
elif [[ "$init_result" == GENERATED:* ]]; then

View File

@ -0,0 +1,12 @@
{
"tools": [
{
"id": "rotate_bouncer_key",
"category": "security",
"label": "Rotate Traefik Bouncer Key",
"description": "Issue a new API key for the Traefik bouncer and restart Traefik to load it. Use this if the key is lost or may have been exposed. Traefik briefly rejects bouncer checks while it restarts.",
"icon": "🔑",
"fields": []
}
]
}

View File

@ -0,0 +1,50 @@
#!/bin/bash
# Issue a new API key for the Traefik CrowdSec bouncer.
#
# This is the action crowdsec.config and the install script both point at when
# the key is lost or should be replaced. cscli cannot re-issue a key for an
# existing bouncer, so rotating means delete + re-add; the privileged helper
# does both and rewrites /etc/crowdsec/traefik_bouncer.key.
#
# The old key stops working the instant the bouncer is deleted, and Traefik
# holds the key file open — so Traefik is restarted afterwards to pick up the new
# one. Between those two points requests are authenticated with a dead key, which
# is why this is a deliberate action and not something the installer does on its
# own.
appCrowdsecRotateBouncerKey()
{
local app_name="crowdsec"
local result
result=$(runCrowdsec bouncer-traefik-rotate 2>&1)
if [[ "$result" != GENERATED:* ]]; then
isError "Could not rotate the Traefik bouncer key: $result"
isNotice "The previous key may already have been revoked — check 'cscli bouncers list' before retrying."
return 1
fi
local bouncer_key="${result#GENERATED:}"
isSuccessful "New Traefik bouncer API key issued."
# Mirror it the same way the installer does, so the config page and the key
# file agree. updateConfigOption escapes the value, writes as the owner of
# the containers tree, and re-sources.
local cfg_file="${containers_dir}${app_name}/${app_name}.config"
if [[ -f "$cfg_file" ]]; then
updateConfigOption "CFG_CROWDSEC_TRAEFIK_LAPI_KEY" "$bouncer_key" "$cfg_file"
else
isNotice "crowdsec.config is not deployed — the key is in /etc/crowdsec/traefik_bouncer.key only."
fi
# Traefik reads the key from the bind-mounted file at startup, so it keeps
# presenting the revoked key until it restarts. Without this the rotation
# looks successful while every bouncer check fails with 403.
if [[ -d "${containers_dir}traefik" ]]; then
dockerComposeRestart traefik
checkSuccess "Restarting Traefik to load the new bouncer key"
else
isNotice "Traefik is not installed here — nothing to restart."
fi
}

View File

@ -646,3 +646,27 @@
with siblings. .form-group:last-child rule already handles the bottom. */
.tool-form > .tool-form-toggle { margin-bottom: 14px; }
.tool-form > .tool-form-toggle:last-child { margin-bottom: 0; }
/* Tool result modal the outcome lines a tool printed, shown in place so a
short admin action doesn't have to send the user to the Tasks tab to find
out what happened. Monospace because these lines carry generated passwords
and user IDs that get copied out by hand. */
.tool-result-lines { display: flex; flex-direction: column; gap: 6px; }
.tool-result-line {
padding: 9px 11px;
background: rgba(var(--text-rgb), 0.03);
border: 1px solid rgba(var(--text-rgb), 0.07);
border-left: 3px solid var(--success, #3fb950);
border-radius: 8px;
font-size: 13px;
line-height: 1.45;
color: var(--text-primary);
font-family: ui-monospace, "SF Mono", Menlo, monospace;
/* Long IDs and generated passwords must stay selectable and fully visible
rather than being clipped. */
overflow-wrap: anywhere;
}
.tool-result-line.is-error {
border-left-color: var(--danger, #f85149);
background: rgba(248, 81, 73, 0.06);
}

View File

@ -827,6 +827,80 @@ class ToolsManager {
return pairs.join('|');
}
// Wait for one specific tool task to finish, then surface its outcome
// without leaving the Tools tab.
//
// list_users is left alone: _maybeOpenUserListModal already listens for the
// same event and opens the interactive account list, which is a better
// result view than a summary could be.
_watchToolTask(tool, taskId) {
if (!taskId) return;
const onDone = (ev) => {
const d = ev?.detail || {};
const id = d.taskId || d.id || d.task?.id;
if (id !== taskId) return; // not ours — keep listening
window.removeEventListener('taskCompleted', onDone);
if (tool.id === 'list_users') return;
this._showToolResult(tool, taskId, d.status);
};
window.addEventListener('taskCompleted', onDone);
// Don't leak the listener if the task never reports back (processor
// restart, page left open for hours).
setTimeout(() => window.removeEventListener('taskCompleted', onDone), 10 * 60 * 1000);
}
// Pull the tool's own result lines out of the task log. The adapters speak in
// isSuccessful/isError/isNotice, which land as ✓/✗/! prefixed lines wrapped
// in ANSI colour — exactly the lines worth showing and nothing else.
async _showToolResult(tool, taskId, status) {
let lines = [];
try {
const r = await fetch(`/read-file?path=tasks/${encodeURIComponent(taskId)}.log`, { cache: 'no-store' });
if (r.ok) {
const text = (await r.text()).replace(/\x1b\[[0-9;]*m/g, '');
lines = text.split(/\r?\n/)
.map(l => l.trim())
.filter(l => /^[!]\s/.test(l))
// Drop the framework's own boilerplate — it is noise here, not result.
.filter(l => !/config files are successfully set up|Detected OS/i.test(l));
}
} catch (_) { /* fall through to the generic message */ }
const failed = status === 'failed' || lines.some(l => l.startsWith('✗'));
const body = lines.length
? `<div class="tool-result-lines">${lines.map(l => `<div class="tool-result-line${l.startsWith('✗') ? ' is-error' : ''}">${escapeHtml(l)}</div>`).join('')}</div>`
: window.eoEmpty(failed ? 'The tool reported a failure — open the log for detail.' : 'Finished with no output.');
window.openEoModal({
id: 'tool-result-modal',
size: 'sm',
icon: this.currentApp ? `/core/icons/apps/${encodeURIComponent(this.currentApp)}.svg` : '',
iconAlt: this.currentApp || '',
eyebrow: failed ? 'Failed' : 'Done',
title: tool.label || tool.id,
body,
actions: [
// An action with an onClick suppresses the modal's own auto-close,
// so close it explicitly before navigating.
{ label: 'View log', variant: 'secondary', onClick: (m) => { m.close(); this._openTaskLog(taskId); } },
{ label: 'Close' }
]
});
}
// The escape hatch to the full log, for when the summary isn't enough.
_openTaskLog(taskId) {
if (!window.appTabbedManager) return;
window.appTabbedManager.switchTab('tasks');
setTimeout(() => {
const tm = window.appTabbedManager.tasksManager;
if (tm) {
tm.highlightedTaskId = taskId;
tm.renderTasks();
}
}, 300);
}
async _dispatch(tool, toolArgs) {
if (!this.currentApp) return;
if (!window.tasksManager || !window.tasksManager.router) {
@ -844,19 +918,18 @@ class ToolsManager {
toolLabel: tool.label || tool.id
});
// Mirror the install flow: jump to the Tasks tab and auto-expand
// the new task so the user sees its log streaming in.
setTimeout(() => {
if (window.appTabbedManager) {
window.appTabbedManager.switchTab('tasks');
setTimeout(() => {
if (task && window.appTabbedManager.tasksManager) {
window.appTabbedManager.tasksManager.highlightedTaskId = task.id;
window.appTabbedManager.tasksManager.renderTasks();
}
}, 300);
}
}, 200);
// Deliberately NOT the install flow's jump-to-Tasks.
//
// An install is a long, log-heavy thing you want to watch. A tool is a
// short admin action whose answer is one line — and half of them are only
// meaningful back here: List Users opens a modal over the Tools tab, and
// Create User Account returns a generated password. Throwing the user
// onto the Tasks tab for those loses the context they were working in and
// buries the result in a log they then have to read.
//
// So: stay put, and bring the result to them. The log is still one click
// away from the completion notice for anything that needs the detail.
this._watchToolTask(tool, task && task.id);
} catch (err) {
console.error('Tool dispatch failed', err);
if (window.notificationSystem) {

View File

@ -227,7 +227,9 @@ for u in res.get('users', []):
# EZ_USER<TAB>identifier<TAB>display<TAB>roles — the exact shape the WebUI's
# user-list modal parses. The first column is what a row action gets
# prefilled with, so it must be the Matrix ID, not the display name.
print('EZ_USER\t' + u['name'] + '\t' + (u.get('displayname') or '') + '\t' + (','.join(flags) or 'user'))
name = u.get('displayname') or ''
print('EZ_USER\t' + u['name'] + '\t' + name + '\t' + (','.join(flags) or 'user'))
print(' %-34s %-20s %s' % (u['name'], name or '-', ','.join(flags) or 'user'))
print('LP_TOTAL:' + str(res.get('total', 0)))
" 2>&1)
@ -236,13 +238,13 @@ print('LP_TOTAL:' + str(res.get('total', 0)))
local line total=0
while IFS= read -r line; do
case "$line" in
EZ_USER*) IFS=$'\t' read -r _ uid name flags <<< "$line"
# Re-emit the raw marker line as well as the readable one:
# the WebUI modal reads the task log looking for EZ_USER,
# so consuming it here and printing only the pretty version
# left the modal with nothing to parse.
printf '%s\n' "$line"
printf ' %-34s %-20s %s\n' "$uid" "$name" "$flags" ;;
# Both the marker line (for the WebUI modal, which reads the task
# log) and the aligned line (for a human) are printed by the python
# above — pass them straight through. Re-splitting the marker here
# used to shift the columns whenever a field was empty, because TAB
# is IFS whitespace and bash collapses a run of it into one
# delimiter.
EZ_USER*|' '*) printf '%s\n' "$line" ;;
LP_TOTAL:*) total="${line#LP_TOTAL:}" ;;
esac
done <<< "$out"

View File

@ -161,7 +161,7 @@ are preserved from then on.
The convention is now uniform: **if a config key holds a generated value, its name
ends in a slot number.** `CFG_<APP>_DB_PASSWORD` became
`CFG_<APP>_DB_PASSWORD_1`, `CFG_STALWART_ADMIN_PASSWORD` became
`CFG_STALWART_ADMIN_PASSWORD_1`, and so on — 38 keys across the catalog. An app
`CFG_STALWART_ADMIN_PASSWORD_1`, and so on — 39 keys across the catalog. An app
that needs a second credential of the same kind just adds `_2`; nothing has to be
registered, because the tag name is derived from the key.

View File

@ -175,8 +175,8 @@ runAppCfg() { _runRootHelper libreportal-appcfg "$@"; }
# touch+chmod, /etc/crowdsec/traefik_bouncer.key write. One audit funnel for
# every operation the host-side CrowdSec install needs the manager can't drop:
# {install|services <enable|disable|restart>|capi <register|unregister|status>
# |console <enroll <token>|disenroll|status>|bouncer-traefik-init|
# bouncer-priority|bind-lapi|prometheus <on <addr> <port>|off>|touch-host-logs}
# |console <enroll <token>|disenroll|status>|bouncer-traefik-init|bouncer-traefik-rotate|
# bouncer-priority|bind-lapi <addr:port>|prometheus <on <addr> <port>|off>|touch-host-logs}
runCrowdsec() { _runRootHelper libreportal-crowdsec "$@"; }
# Genuine system-administration command (ufw/systemctl/apt/sysctl/useradd, /etc

View File

@ -0,0 +1,9 @@
#!/bin/bash
# This file is auto-generated by generate_arrays.sh
# Do not edit manually - run './scripts/source/files/generate_arrays.sh run' to regenerate
validation_scripts=(
"validation/validate_config.sh"
)

View File

@ -31,6 +31,7 @@ declare -gA LP_FN_MAP=(
[appCrowdSecFixPriority]="crowdsec/scripts/crowdsec_fix_priority.sh"
[appCrowdSecHubUpdate]="crowdsec/scripts/crowdsec_hub_update.sh"
[appCrowdSecMetrics]="crowdsec/scripts/crowdsec_metrics.sh"
[appCrowdsecRotateBouncerKey]="crowdsec/tools/crowdsec_rotate_bouncer_key.sh"
[appCrowdSecStatus]="crowdsec/scripts/crowdsec_status.sh"
[appCrowdSecUnban]="crowdsec/scripts/crowdsec_unban.sh"
[appCrowdSecUpdate]="crowdsec/scripts/crowdsec_update.sh"
@ -1175,6 +1176,7 @@ declare -gA LP_FN_ROOT=(
[appCrowdSecFixPriority]="containers"
[appCrowdSecHubUpdate]="containers"
[appCrowdSecMetrics]="containers"
[appCrowdsecRotateBouncerKey]="containers"
[appCrowdSecStatus]="containers"
[appCrowdSecUnban]="containers"
[appCrowdSecUpdate]="containers"
@ -2355,6 +2357,7 @@ appCrowdSecDecisionsList() { unset -f appCrowdSecDecisionsList; __lpAutoload "${
appCrowdSecFixPriority() { unset -f appCrowdSecFixPriority; __lpAutoload "${install_containers_dir}crowdsec/scripts/crowdsec_fix_priority.sh"; appCrowdSecFixPriority "$@"; }
appCrowdSecHubUpdate() { unset -f appCrowdSecHubUpdate; __lpAutoload "${install_containers_dir}crowdsec/scripts/crowdsec_hub_update.sh"; appCrowdSecHubUpdate "$@"; }
appCrowdSecMetrics() { unset -f appCrowdSecMetrics; __lpAutoload "${install_containers_dir}crowdsec/scripts/crowdsec_metrics.sh"; appCrowdSecMetrics "$@"; }
appCrowdsecRotateBouncerKey() { unset -f appCrowdsecRotateBouncerKey; __lpAutoload "${install_containers_dir}crowdsec/tools/crowdsec_rotate_bouncer_key.sh"; appCrowdsecRotateBouncerKey "$@"; }
appCrowdSecStatus() { unset -f appCrowdSecStatus; __lpAutoload "${install_containers_dir}crowdsec/scripts/crowdsec_status.sh"; appCrowdSecStatus "$@"; }
appCrowdSecUnban() { unset -f appCrowdSecUnban; __lpAutoload "${install_containers_dir}crowdsec/scripts/crowdsec_unban.sh"; appCrowdSecUnban "$@"; }
appCrowdSecUpdate() { unset -f appCrowdSecUpdate; __lpAutoload "${install_containers_dir}crowdsec/scripts/crowdsec_update.sh"; appCrowdSecUpdate "$@"; }

View File

@ -66,9 +66,19 @@ for folder in "$SCRIPTS_DIR"/*; do
folder_name=$(basename "$folder")
# Skip folders that aren't sourced function libraries: the dead-code
# graveyard; system/ (standalone root-owned helpers invoked via sudo); and
# release/ (build tooling — make_release.sh runs at build time, never sourced).
if [ "$folder_name" = "unused" ] || [ "$folder_name" = "system" ] || [ "$folder_name" = "release" ]; then
# graveyard; system/ (standalone root-owned helpers invoked via sudo);
# release/ (build tooling — make_release.sh runs at build time, never
# sourced); and dev/ (maintainer-only tools such as lp-shot).
#
# dev/ matters more than it looks. It is `export-ignore`d in
# .gitattributes, so it exists in a working clone but never in a shipped
# install. Generating a files_dev.sh entry from it writes a reference
# into files_source.sh that no install can satisfy — and the loader
# treats a missing array file as a broken installation, so every
# libreportal command then stops with "It seems that files_dev.sh is
# missing from your LibrePortal Installation".
if [ "$folder_name" = "unused" ] || [ "$folder_name" = "system" ] \
|| [ "$folder_name" = "release" ] || [ "$folder_name" = "dev" ]; then
isNotice "Skipping $folder_name/"
continue
fi

View File

@ -100,15 +100,10 @@ crowdsec_console() {
esac
}
# --- bouncer-traefik-init: cscli register + write key file --------------------
# Idempotent: if the bouncer is already registered, skips and prints the existing
# key-file marker so callers can decide whether to keep or rotate. Returns the
# fresh key on stdout when newly generated, "EXISTS" when already registered.
crowdsec_bouncer_traefik_init() {
if cscli bouncers list -o raw 2>/dev/null | grep -q '^traefik-bouncer'; then
echo "EXISTS"
return 0
fi
# --- traefik bouncer: register + write key file -------------------------------
# Shared by init and rotate: registers the bouncer with cscli and writes the key
# file. Prints GENERATED:<key> so the caller can mirror it into the config.
_crowdsec_bouncer_traefik_add() {
local key
key=$(cscli bouncers add traefik-bouncer -o raw 2>&1 | tail -1)
[[ -n "$key" && "$key" != *"error"* ]] || {
@ -123,15 +118,49 @@ crowdsec_bouncer_traefik_init() {
echo "GENERATED:$key"
}
# --- bind-lapi: set listen_uri to 0.0.0.0:8080 in config.yaml -----------------
# Traefik talks to LAPI via host.docker.internal:8080. Bouncer API key gates
# init: idempotent. "EXISTS" when the bouncer is already registered (the caller
# recovers the key from the key file), GENERATED:<key> when newly created.
crowdsec_bouncer_traefik_init() {
if cscli bouncers list -o raw 2>/dev/null | grep -q '^traefik-bouncer'; then
echo "EXISTS"
return 0
fi
_crowdsec_bouncer_traefik_add
}
# --- bouncer-traefik-rotate: issue a new key for the Traefik bouncer ----------
# Delete then re-add: cscli cannot re-issue a key for an existing bouncer, and it
# cannot print an existing one either. The old key stops working the moment the
# bouncer is deleted, so the caller must restart Traefik to pick up the new one —
# there is a window where Traefik authenticates with a dead key.
crowdsec_bouncer_traefik_rotate() {
if cscli bouncers list -o raw 2>/dev/null | grep -q '^traefik-bouncer'; then
cscli bouncers delete traefik-bouncer >/dev/null 2>&1 || {
echo "libreportal-crowdsec: could not delete the existing traefik-bouncer" >&2
return 1
}
fi
_crowdsec_bouncer_traefik_add
}
# --- bind-lapi <addr:port>: set listen_uri in config.yaml ---------------------
# Traefik talks to LAPI via host.docker.internal. The bouncer API key gates
# external access (HTTP 401 without it).
# Takes the bind target as <addr>:<port>, validated the same way the prometheus
# action validates its own, so the scoped sudoers still only ever sees a fixed
# edit. Defaults to 0.0.0.0:8080 when the caller passes nothing.
crowdsec_bind_lapi() {
local target="${1:-0.0.0.0:8080}"
local addr="${target%%:*}" port="${target##*:}"
[[ "$addr" =~ ^[A-Za-z0-9.-]+$ ]] || { echo "libreportal-crowdsec: bind-lapi invalid addr" >&2; return 1; }
[[ "$port" =~ ^[0-9]+$ ]] || { echo "libreportal-crowdsec: bind-lapi invalid port" >&2; return 1; }
[[ -f "$CFG_FILE" ]] || { echo "libreportal-crowdsec: $CFG_FILE not found" >&2; return 1; }
if grep -qE 'listen_uri:[[:space:]]*0\.0\.0\.0:8080' "$CFG_FILE"; then
local esc_addr="${addr//./\\.}"
if grep -qE "listen_uri:[[:space:]]*${esc_addr}:${port}([[:space:]]|$)" "$CFG_FILE"; then
echo "ALREADY_BOUND"; return 0
fi
sed -i 's|listen_uri:.*|listen_uri: 0.0.0.0:8080|' "$CFG_FILE"
sed -i "s|listen_uri:.*|listen_uri: ${addr}:${port}|" "$CFG_FILE"
}
# --- prometheus {on <addr> <port>|off} ----------------------------------------
@ -191,12 +220,13 @@ case "$action" in
capi) crowdsec_capi "${1:-}" ;;
console) crowdsec_console "${1:-}" "${2:-}" ;;
bouncer-traefik-init) crowdsec_bouncer_traefik_init ;;
bouncer-traefik-rotate) crowdsec_bouncer_traefik_rotate ;;
bouncer-priority) crowdsec_bouncer_priority ;;
bind-lapi) crowdsec_bind_lapi ;;
bind-lapi) crowdsec_bind_lapi "${1:-}" ;;
prometheus) crowdsec_prometheus "${1:-}" "${2:-}" "${3:-}" ;;
touch-host-logs) crowdsec_touch_host_logs ;;
*)
echo "usage: libreportal-crowdsec {install|services <enable|disable|restart>|capi <register|unregister|status>|console <enroll <token>|disenroll|status>|bouncer-traefik-init|bouncer-priority|bind-lapi|prometheus <on <addr> <port>|off>|touch-host-logs}" >&2
echo "usage: libreportal-crowdsec {install|services <enable|disable|restart>|capi <register|unregister|status>|console <enroll <token>|disenroll|status>|bouncer-traefik-init|bouncer-traefik-rotate|bouncer-priority|bind-lapi <addr:port>|prometheus <on <addr> <port>|off>|touch-host-logs}" >&2
exit 2
;;
esac

View File

@ -959,13 +959,6 @@ PORTEOF
"tooltip": "dpkg package name.",
"readonly": true
},
"HOST_SERVICE": {
"category": "advanced",
"label": "Primary unit",
"type": "text",
"tooltip": "systemd unit for stop/restart.",
"readonly": true
},
"HOST_SERVICES": {
"category": "advanced",
"label": "All units",