WebUI-created tasks emit camelCase initial fields (createdAt, startedAt,
completedAt, heartbeatAt, exitCode, errorMessage) per
tasks-manager.js / task-manager.js conventions, with createdAt in
ISO-UTC-with-ms (`2026-05-27T13:01:26.345Z`). The processor then layers
snake_case status fields (started_at, heartbeat_at, …) on top as the
task runs.
The CLI's cliTaskRun was writing snake_case only — `created_at` with
local-tz offset. The task panel's renderer reads `task.createdAt`
directly (no alias), so CLI-queued tasks showed blank Created/Started
columns until the processor wrote its own snake_case overlay
(which doesn't include createdAt at all). Visible symptom: dates
"broken" on CLI-queued tasks.
Now the initial JSON cliTaskRun writes matches what the WebUI's
"Install" button writes:
{
id, command, status: queued,
createdAt: "<ISO-UTC-with-ms>",
startedAt: null, completedAt: null, heartbeatAt: null,
exitCode: null, errorMessage: null,
type, app
}
Processor side is unchanged (still adds snake_case overlay on
status transitions — that's how WebUI tasks already work). No JSON
shape change for in-flight tasks.
ALSO (out-of-repo): /home/user/Documents/Scripts/update.sh now restarts
the systemd `libreportal.service` task processor after the docker
`libreportal-service` container restart. Same reason — both pre-load
code at startup, both need a restart to pick up changes. Without this,
deploys silently kept a stale processor running old code while the
disk reflected the new code; the install task-routing recursion I just
saw was a direct consequence.
Signed-off-by: librelad <librelad@digitalangels.vip>
189 lines
7.6 KiB
Bash
189 lines
7.6 KiB
Bash
#!/bin/bash
|
|
|
|
# Routes a CLI command through the existing libreportal task processor and
|
|
# follows its log in the foreground, so the terminal sees the same output
|
|
# the WebUI sees and Ctrl-C detaches cleanly without killing the task.
|
|
#
|
|
# Usage:
|
|
# cliTaskRun "libreportal app install dashy" install dashy
|
|
# cliTaskRun "libreportal app install dashy" install dashy --detach
|
|
#
|
|
# Sets up a task file that's identical to what the WebUI's "install" button
|
|
# produces — same format, same task dir, same processor. The CLI just adds
|
|
# itself as a viewer.
|
|
#
|
|
# How the command bypass works: the recursive command in the task file is
|
|
# prefixed with `LIBREPORTAL_TASK_EXEC=1`, and the caller (the app install
|
|
# handler) honours that by running inline instead of re-enqueueing. No
|
|
# processor changes required — the bypass travels with the task.
|
|
|
|
# Resolve task dir, FIFO, the manifest of tasks dir/log paths. Mirrors what
|
|
# crontab_task_processor.sh and webui_task_create.sh use; if those move, both
|
|
# this file and they have to update together.
|
|
_taskDir() {
|
|
echo "${containers_dir}libreportal/frontend/data/tasks"
|
|
}
|
|
|
|
_genTaskId() {
|
|
local rand
|
|
rand=$(openssl rand -hex 4 2>/dev/null || printf '%08x' $RANDOM)
|
|
echo "task_$(date +%s)_${rand}"
|
|
}
|
|
|
|
# Write the task file + append to queue.json. Identical shape to
|
|
# createTaskFile in webui_task_create.sh, but inlined here so the CLI doesn't
|
|
# have to source the WebUI generators when LP_LAZY=1 hasn't pulled them in.
|
|
_writeTaskFile() {
|
|
local task_id="$1" command="$2" task_type="$3" app_name="$4"
|
|
local task_dir; task_dir=$(_taskDir)
|
|
local task_file="$task_dir/${task_id}.json"
|
|
# ISO-8601 UTC with milliseconds. Matches the format the WebUI's
|
|
# tasks-manager.js emits (Date.now() → toISOString()), so the tasks
|
|
# panel's `new Date(task.createdAt).toLocaleString()` renders correctly.
|
|
# Falls back to seconds-precision if %3N isn't supported (busybox).
|
|
local created_at
|
|
created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ" 2>/dev/null) \
|
|
|| created_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
[[ -d "$task_dir" ]] || runFileOp mkdir -p "$task_dir"
|
|
|
|
# CamelCase initial fields match the WebUI's task shape; processor adds
|
|
# snake_case status fields on top as it runs. Both task-manager.js and
|
|
# tasks-manager.js read camelCase directly (no alias needed for createdAt).
|
|
local task_json
|
|
if command -v jq >/dev/null 2>&1; then
|
|
task_json=$(jq -n \
|
|
--arg id "$task_id" \
|
|
--arg command "$command" \
|
|
--arg status queued \
|
|
--arg createdAt "$created_at" \
|
|
--arg type "${task_type:-}" \
|
|
--arg app "${app_name:-}" \
|
|
'{id:$id, command:$command, status:$status, createdAt:$createdAt,
|
|
startedAt:null, completedAt:null, heartbeatAt:null,
|
|
exitCode:null, errorMessage:null}
|
|
+ (if $type != "" then {type:$type} else {} end)
|
|
+ (if $app != "" then {app:$app} else {} end)')
|
|
else
|
|
# Plain string interpolation — command is the only field that needs
|
|
# escaping. Strip control chars; the dispatcher uses ascii only so
|
|
# this is sufficient without pulling jq in.
|
|
local esc="${command//\\/\\\\}"; esc="${esc//\"/\\\"}"
|
|
task_json="{\"id\":\"$task_id\",\"command\":\"$esc\",\"status\":\"queued\",\"createdAt\":\"$created_at\""
|
|
task_json+=",\"startedAt\":null,\"completedAt\":null,\"heartbeatAt\":null"
|
|
task_json+=",\"exitCode\":null,\"errorMessage\":null"
|
|
[[ -n "$task_type" ]] && task_json+=",\"type\":\"$task_type\""
|
|
[[ -n "$app_name" ]] && task_json+=",\"app\":\"$app_name\""
|
|
task_json+="}"
|
|
fi
|
|
|
|
printf '%s' "$task_json" | runFileWrite "$task_file"
|
|
runFileOp chmod 644 "$task_file" 2>/dev/null
|
|
|
|
local queue="$task_dir/queue.json"
|
|
[[ -f "$queue" ]] || printf '[]' | runFileWrite "$queue"
|
|
if command -v jq >/dev/null 2>&1; then
|
|
local updated; updated=$(jq --arg id "$task_id" '. + [$id]' "$queue" 2>/dev/null || echo "[\"$task_id\"]")
|
|
printf '%s' "$updated" | runFileWrite "$queue"
|
|
else
|
|
# Best-effort fallback.
|
|
local cur; cur=$(cat "$queue" 2>/dev/null)
|
|
if [[ "$cur" == "[]" || -z "$cur" ]]; then
|
|
printf '["%s"]' "$task_id" | runFileWrite "$queue"
|
|
else
|
|
printf '%s' "${cur%]}, \"$task_id\"]" | runFileWrite "$queue"
|
|
fi
|
|
fi
|
|
|
|
# Poke the FIFO so the processor dispatches immediately instead of
|
|
# waiting up to IDLE_POLL_SECS (3s) for its next read timeout.
|
|
local fifo="$task_dir/.queue.fifo"
|
|
[[ -p "$fifo" ]] && printf '%s\n' "$task_id" >> "$fifo" 2>/dev/null || true
|
|
}
|
|
|
|
# Follow a task's log + status until terminal state.
|
|
# Returns the task's exit_code (0 on completed, non-zero on failed/cancelled).
|
|
# Ctrl-C from the user detaches the follower without cancelling the task.
|
|
cliTaskFollow() {
|
|
local task_id="$1"
|
|
local task_dir; task_dir=$(_taskDir)
|
|
local task_file="$task_dir/${task_id}.json"
|
|
local log_file="$task_dir/${task_id}.log"
|
|
|
|
[[ -f "$task_file" ]] || { isError "Task $task_id not found."; return 1; }
|
|
|
|
# Ctrl-C detaches us, NOT the task. Print a hint and exit 0 — the
|
|
# processor keeps running in the background and the WebUI continues
|
|
# to show the task. `libreportal task log $task_id` (future) can
|
|
# reattach.
|
|
local detached=0
|
|
trap 'detached=1' INT
|
|
|
|
# tail -F (capital): retry-on-error, so we can start tailing before
|
|
# the log file exists. Processor creates it just before exec.
|
|
tail -n +1 -F "$log_file" 2>/dev/null &
|
|
local tail_pid=$!
|
|
|
|
# Poll the task file until status hits a terminal state OR the user
|
|
# detaches. 0.5s polling — bash + jq is cheap and the UX feels live.
|
|
local status="queued" exit_code=0
|
|
while (( detached == 0 )); do
|
|
if command -v jq >/dev/null 2>&1; then
|
|
status=$(jq -r '.status // "queued"' "$task_file" 2>/dev/null)
|
|
else
|
|
status=$(grep -oE '"status"[[:space:]]*:[[:space:]]*"[^"]+"' "$task_file" 2>/dev/null | sed 's/.*"\([^"]*\)"$/\1/')
|
|
fi
|
|
case "$status" in
|
|
completed|failed|cancelled) break ;;
|
|
esac
|
|
sleep 0.5
|
|
done
|
|
trap - INT
|
|
|
|
# Let tail flush the final output buffered behind the log writer, then
|
|
# kill it cleanly.
|
|
sleep 0.3
|
|
kill "$tail_pid" 2>/dev/null
|
|
wait "$tail_pid" 2>/dev/null
|
|
|
|
if (( detached )); then
|
|
echo ""
|
|
isNotice "Detached from $task_id. Task continues in the background; the WebUI tasks panel will show its progress."
|
|
return 0
|
|
fi
|
|
|
|
if command -v jq >/dev/null 2>&1; then
|
|
exit_code=$(jq -r '.exit_code // 0' "$task_file" 2>/dev/null)
|
|
fi
|
|
case "$status" in
|
|
completed) return "${exit_code:-0}" ;;
|
|
failed) return "${exit_code:-1}" ;;
|
|
cancelled) return 130 ;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
# Enqueue a command and follow until terminal state (or --detach).
|
|
# Args: command_string task_type app_name [--detach]
|
|
cliTaskRun() {
|
|
local command="$1" task_type="$2" app_name="$3" mode="$4"
|
|
|
|
if [[ -z "$command" ]]; then
|
|
isError "cliTaskRun: empty command."
|
|
return 1
|
|
fi
|
|
|
|
local task_id; task_id=$(_genTaskId)
|
|
_writeTaskFile "$task_id" "$command" "$task_type" "$app_name"
|
|
|
|
if [[ "$mode" == "--detach" ]]; then
|
|
isSuccessful "Task queued: $task_id"
|
|
isNotice "Detached — track progress in the WebUI tasks panel or via 'libreportal task log $task_id' (future)."
|
|
return 0
|
|
fi
|
|
|
|
isNotice "Queued task $task_id ($task_type${app_name:+ $app_name}). Following live output — Ctrl-C detaches without cancelling."
|
|
echo ""
|
|
cliTaskFollow "$task_id"
|
|
}
|