a
This commit is contained in:
@@ -91,12 +91,15 @@ PNG/JPG/SVG/GIF из `.\software\` и `.\software\icons\` попадут в
|
||||
1. `dpkg -i debs/*.deb` офлайн (два прохода), `ldconfig`.
|
||||
2. Создаёт пользователя `buttontask`, группы `video render input`.
|
||||
3. Раскладывает `/opt/buttontask/{versions/<ver>,config,icons,run,scripts}`,
|
||||
симлинк `current`. Существующий `config.json` не трогает.
|
||||
симлинк `current`. Существующий `config.json` **по умолчанию не трогает**
|
||||
(upgrade-safe). Для полного сброса: `BUTTONTASK_RESET_CONFIG=1` или
|
||||
`deploy-adb.ps1 -ResetConfig`, либо галочка в sshDeploy GUI.
|
||||
4. Хелперы `bt-*` + `sudoers` (+`visudo -c`).
|
||||
5. `/tmp/runtime-buttontask` через `tmpfiles.d` (переживает ребут).
|
||||
6. Авто-детект тачскрина → `override.conf` с нужным `eventN`.
|
||||
7. Выключает `display-manager` (eglfs забирает KMS/DRM).
|
||||
8. `systemctl enable buttontask buttontask-web buttontask-network`; старт — после ребута.
|
||||
UI (`buttontask`) не ждёт поднятия IP — сеть настраивается параллельно с коротким таймаутом.
|
||||
9. Если в бандле есть `config/.master` — кладёт аварийный мастер-пароль (web + Qt).
|
||||
|
||||
Идемпотентен: повторный прогон безопасен.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,7 +6,8 @@
|
||||
# Сценарий: подключил по USB -> нашлось adb-устройство -> отработал -> ребут -> работает.
|
||||
param(
|
||||
[string]$Bundle,
|
||||
[string]$Adb
|
||||
[string]$Adb,
|
||||
[switch]$ResetConfig
|
||||
)
|
||||
# native-команды adb пишут в stderr (adb root, прогресс push); Stop делает это фатальным
|
||||
$ErrorActionPreference = 'Continue'
|
||||
@@ -67,7 +68,13 @@ Log 'заливаю бандл...'
|
||||
|
||||
# 5. установка, ловим INSTALL-OK
|
||||
Log 'разворачиваю на устройстве...'
|
||||
$cmd = ("$sudo bash /tmp/btdeploy/device-install.sh").Trim()
|
||||
if ($ResetConfig) {
|
||||
Log 'полный сброс config.json и .master из бандла'
|
||||
if ($sudo) { $cmd = "$sudo env BUTTONTASK_RESET_CONFIG=1 bash /tmp/btdeploy/device-install.sh" }
|
||||
else { $cmd = 'env BUTTONTASK_RESET_CONFIG=1 bash /tmp/btdeploy/device-install.sh' }
|
||||
} else {
|
||||
$cmd = ("$sudo bash /tmp/btdeploy/device-install.sh").Trim()
|
||||
}
|
||||
$output = & $Adb shell $cmd 2>&1 | Out-String
|
||||
Write-Host ($output -replace "`r", '')
|
||||
if ($output -notmatch 'INSTALL-OK') { Die 'установка завершилась без INSTALL-OK (см. вывод выше)' }
|
||||
|
||||
@@ -53,8 +53,20 @@ install -m0755 "$BUNDLE_DIR/app/ButtonTask" "$ROOT/versions/$VER/ButtonTask"
|
||||
cp -a "$BUNDLE_DIR/webconfig" "$ROOT/versions/$VER/"
|
||||
ln -sfn "$ROOT/versions/$VER" "$ROOT/current"
|
||||
|
||||
# config — не перезатираем существующий (apgrade-safe)
|
||||
if [ -f "$ROOT/config/config.json" ]; then
|
||||
# config — по умолчанию не перезатираем (upgrade-safe); BUTTONTASK_RESET_CONFIG=1 — из бандла
|
||||
reset_cfg="${BUTTONTASK_RESET_CONFIG:-0}"
|
||||
case "$reset_cfg" in
|
||||
1|yes|true|TRUE|Yes|YES) reset_cfg=1 ;;
|
||||
*) reset_cfg=0 ;;
|
||||
esac
|
||||
if [ "$reset_cfg" = "1" ]; then
|
||||
if [ -f "$BUNDLE_DIR/config/config.json" ]; then
|
||||
cp -a "$BUNDLE_DIR/config/config.json" "$ROOT/config/"
|
||||
log "config.json перезаписан из бандла (полный сброс конфига)"
|
||||
else
|
||||
warn "BUTTONTASK_RESET_CONFIG=1, но config.json в бандле нет"
|
||||
fi
|
||||
elif [ -f "$ROOT/config/config.json" ]; then
|
||||
log "config.json уже есть — оставляю как есть"
|
||||
elif [ -f "$BUNDLE_DIR/config/config.json" ]; then
|
||||
cp -a "$BUNDLE_DIR/config/config.json" "$ROOT/config/"
|
||||
@@ -111,7 +123,11 @@ chown -R "$USER_NAME:$USER_NAME" "$ROOT"
|
||||
chown root:root "$ROOT"/scripts/bt-*
|
||||
|
||||
# мастер-пароль (аварийный вход web + Qt) — один раз при первой установке
|
||||
if [ -f "$ROOT/config/.master" ]; then
|
||||
if [ "$reset_cfg" = "1" ] && [ -f "$BUNDLE_DIR/config/.master" ]; then
|
||||
install -m0600 -o "$USER_NAME" -g "$USER_NAME" \
|
||||
"$BUNDLE_DIR/config/.master" "$ROOT/config/.master"
|
||||
log "мастер-пароль перезаписан из бандла (полный сброс конфига)"
|
||||
elif [ -f "$ROOT/config/.master" ]; then
|
||||
log "config/.master уже есть — оставляю как есть"
|
||||
elif [ -f "$BUNDLE_DIR/config/.master" ]; then
|
||||
install -m0600 -o "$USER_NAME" -g "$USER_NAME" \
|
||||
@@ -168,7 +184,7 @@ systemctl enable buttontask buttontask-web buttontask-network >/dev/null 2>&1 ||
|
||||
|
||||
# применить сеть сразу (не ждать ребута), если есть сохранённый профиль
|
||||
if [ -f "$ROOT/config/network.json" ] && [ -x "$ROOT/scripts/bt-netconfig-boot" ]; then
|
||||
if "$ROOT/scripts/bt-netconfig-boot"; then
|
||||
if BUTTONTASK_NET_ACTIVATE_TIMEOUT=30 "$ROOT/scripts/bt-netconfig-boot"; then
|
||||
log "сеть применена из network.json"
|
||||
else
|
||||
warn "не удалось применить network.json — проверьте nmcli и кабель"
|
||||
|
||||
@@ -93,13 +93,14 @@ else {
|
||||
"layout": { "mode": "grid", "columns": 2, "spacing": 10, "showLabels": true },
|
||||
"background": { "type": "gradient", "color1": "#00007b", "color2": "#7b007b", "animated": true, "imagePath": "" },
|
||||
"feedback": { "okColor": "#00cc44", "errorColor": "#cc2200", "okWidth": 6, "errorWidth": 6, "glowRadius": 20 },
|
||||
"settings": { "darkMode": true, "password": "admin", "kioskMode": false, "iconsDir": "/opt/buttontask/icons", "webPort": 8080 },
|
||||
"settings": { "darkMode": true, "password": "admin_26", "kioskMode": false, "iconsDir": "/opt/buttontask/icons", "webPort": 8080, "brightness": 60 },
|
||||
"buttons": [
|
||||
{
|
||||
"id": "cleaning-call",
|
||||
"label": "Вызов клининга",
|
||||
"iconPath": "cleaner1Crop.png",
|
||||
"action": { "type": "http_get", "url": "http://192.168.1.55", "headers": {}, "body": "" },
|
||||
"action": { "type": "http_get", "url": "http://192.168.30.147:80/api/click/btn?bid=01*04*01", "headers": {}, "body": "" },
|
||||
"latchReset": { "enabled": true, "resetUrl": "http://192.168.30.147:80/api/click/btn?bid=01*04*00", "successMatch": "OK" },
|
||||
"feedback": { "successText": "Вызов отправлен", "errorText": "Ошибка", "pendingText": "...", "fadeMs": 5000 },
|
||||
"trigger": { "mode": "hold", "holdMs": 800 },
|
||||
"color": "#7b007b"
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"version": 2,
|
||||
"layout": {
|
||||
"mode": "list",
|
||||
"columns": 2,
|
||||
"spacing": 10,
|
||||
"showLabels": true,
|
||||
"labelColor": "#ffffff"
|
||||
},
|
||||
"background": {
|
||||
"type": "solid",
|
||||
"color1": "#000000",
|
||||
"color2": "#7b007b",
|
||||
"animated": true,
|
||||
"imagePath": ""
|
||||
},
|
||||
"feedback": {
|
||||
"okColor": "#00cc44",
|
||||
"errorColor": "#cc2200",
|
||||
"okWidth": 6,
|
||||
"errorWidth": 6,
|
||||
"glowRadius": 20,
|
||||
"holdColor": "#ffffff",
|
||||
"pendingColor": "#ffffff",
|
||||
"pendingWidth": 6
|
||||
},
|
||||
"settings": {
|
||||
"darkMode": true,
|
||||
"password": "admin_26",
|
||||
"kioskMode": false,
|
||||
"iconsDir": "/opt/buttontask/icons",
|
||||
"webPort": 8080,
|
||||
"brightness": 60
|
||||
},
|
||||
"buttons": [
|
||||
{
|
||||
"id": "cleaning-call",
|
||||
"label": "Вызов клининга",
|
||||
"iconPath": "cleaner1Crop.png",
|
||||
"action": {
|
||||
"type": "http_get",
|
||||
"url": "http://192.168.30.147:80/api/click/btn?bid=01*04*01",
|
||||
"headers": {},
|
||||
"body": "",
|
||||
"timeoutMs": 15000
|
||||
},
|
||||
"latchReset": {
|
||||
"enabled": true,
|
||||
"resetUrl": "http://192.168.30.147:80/api/click/btn?bid=01*04*00",
|
||||
"successMatch": "OK",
|
||||
"fireAndForget": true
|
||||
},
|
||||
"feedback": {
|
||||
"successText": "Вызов отправлен",
|
||||
"errorText": "Ошибка",
|
||||
"pendingText": "...",
|
||||
"fadeMs": 5000
|
||||
},
|
||||
"trigger": {
|
||||
"mode": "hold",
|
||||
"holdMs": 800
|
||||
},
|
||||
"color": "#000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -81,6 +81,41 @@ def _validate_response_check(data):
|
||||
}
|
||||
|
||||
|
||||
def _validate_latch_reset(data):
|
||||
if data is None:
|
||||
return None
|
||||
if not isinstance(data, dict):
|
||||
abort(400, "latchReset must be an object")
|
||||
|
||||
enabled = bool(data.get("enabled", False))
|
||||
if not enabled:
|
||||
return {"enabled": False}
|
||||
|
||||
reset_url = (data.get("resetUrl") or "").strip()
|
||||
if not reset_url:
|
||||
abort(400, "latchReset.resetUrl is required when enabled")
|
||||
success_match = (data.get("successMatch") or "OK").strip() or "OK"
|
||||
result = {
|
||||
"enabled": True,
|
||||
"resetUrl": reset_url,
|
||||
"successMatch": success_match,
|
||||
}
|
||||
if data.get("fireAndForget"):
|
||||
result["fireAndForget"] = True
|
||||
return result
|
||||
|
||||
|
||||
def _apply_button_latch_reset(btn, data):
|
||||
if "latchReset" not in data:
|
||||
return btn
|
||||
lr = _validate_latch_reset(data.get("latchReset"))
|
||||
if lr is None:
|
||||
btn.pop("latchReset", None)
|
||||
else:
|
||||
btn["latchReset"] = lr
|
||||
return btn
|
||||
|
||||
|
||||
def _apply_button_action(action, data):
|
||||
action["type"] = data.get("actionType", action.get("type", "http_get"))
|
||||
action["url"] = data.get("url", action.get("url", ""))
|
||||
@@ -88,6 +123,17 @@ def _apply_button_action(action, data):
|
||||
action["headers"] = data["headers"]
|
||||
if "body" in data:
|
||||
action["body"] = data["body"]
|
||||
if "timeoutMs" in data:
|
||||
try:
|
||||
timeout_ms = int(data["timeoutMs"])
|
||||
except (TypeError, ValueError):
|
||||
abort(400, "timeoutMs must be an integer")
|
||||
if timeout_ms < 1000 or timeout_ms > 600000:
|
||||
abort(400, "timeoutMs must be between 1000 and 600000")
|
||||
if timeout_ms == 7000:
|
||||
action.pop("timeoutMs", None)
|
||||
else:
|
||||
action["timeoutMs"] = timeout_ms
|
||||
if "responseCheck" in data:
|
||||
rc = _validate_response_check(data.get("responseCheck"))
|
||||
if rc is None:
|
||||
@@ -148,6 +194,7 @@ def api_add_button():
|
||||
},
|
||||
"color": data.get("color", "#7b007b"),
|
||||
}
|
||||
_apply_button_latch_reset(btn, data)
|
||||
cfg.setdefault("buttons", []).append(btn)
|
||||
save_config(cfg)
|
||||
return jsonify(btn)
|
||||
@@ -180,6 +227,7 @@ def api_update_button(bid):
|
||||
trigger.setdefault("holdMs", 800)
|
||||
if "color" in data:
|
||||
b["color"] = data["color"]
|
||||
_apply_button_latch_reset(b, data)
|
||||
save_config(cfg)
|
||||
return jsonify(b)
|
||||
abort(404)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Description=ButtonTask network bringup
|
||||
After=NetworkManager.service
|
||||
Wants=NetworkManager.service
|
||||
Before=buttontask-web.service buttontask.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Unit]
|
||||
Description=ButtonTask Web Configurator
|
||||
After=buttontask-network.service
|
||||
Wants=buttontask-network.service
|
||||
After=NetworkManager.service
|
||||
Wants=NetworkManager.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Unit]
|
||||
Description=ButtonTask Qt5 UI
|
||||
After=buttontask-network.service
|
||||
Wants=buttontask-network.service
|
||||
After=NetworkManager.service
|
||||
Wants=NetworkManager.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
@@ -50,10 +50,11 @@ def _defaults():
|
||||
},
|
||||
"settings": {
|
||||
"darkMode": True,
|
||||
"password": "admin",
|
||||
"password": "admin_26",
|
||||
"kioskMode": False,
|
||||
"iconsDir": "./icons",
|
||||
"webPort": 8080,
|
||||
"brightness": 60,
|
||||
},
|
||||
"buttons": [],
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
#
|
||||
# Intended to be invoked through sudo by the (unprivileged) web configurator.
|
||||
# Do NOT use ip/ifconfig directly — NetworkManager will undo manual changes.
|
||||
#
|
||||
# Environment (optional):
|
||||
# BUTTONTASK_NET_BOOT=1 boot path: succeed even if activation times out
|
||||
# BUTTONTASK_NET_ACTIVATE_TIMEOUT=N seconds to wait for nmcli connection up (default 30)
|
||||
set -euo pipefail
|
||||
|
||||
err() { echo "bt-netconfig: $*" >&2; exit 1; }
|
||||
@@ -17,6 +21,8 @@ err() { echo "bt-netconfig: $*" >&2; exit 1; }
|
||||
IFACE="$1"
|
||||
MODE="$2"
|
||||
PROFILE="buttontask-$IFACE"
|
||||
BOOT="${BUTTONTASK_NET_BOOT:-0}"
|
||||
ACTIVATE_TIMEOUT="${BUTTONTASK_NET_ACTIVATE_TIMEOUT:-30}"
|
||||
|
||||
[[ "$IFACE" =~ ^[A-Za-z0-9_.:-]{1,32}$ ]] || err "invalid interface name"
|
||||
command -v nmcli >/dev/null 2>&1 || err "nmcli not found"
|
||||
@@ -99,8 +105,25 @@ esac
|
||||
nmcli device disconnect "$IFACE" 2>/dev/null || true
|
||||
nmcli connection down "$CON" 2>/dev/null || true
|
||||
|
||||
if ! nmcli connection up "$CON" ifname "$IFACE" 2>/dev/null; then
|
||||
nmcli connection up "$CON" >/dev/null
|
||||
activate_connection() {
|
||||
local con="$1" iface="$2"
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout "$ACTIVATE_TIMEOUT" nmcli connection up "$con" ifname "$iface" 2>/dev/null \
|
||||
&& return 0
|
||||
timeout "$ACTIVATE_TIMEOUT" nmcli connection up "$con" >/dev/null 2>&1 \
|
||||
&& return 0
|
||||
return 1
|
||||
fi
|
||||
nmcli connection up "$con" ifname "$iface" 2>/dev/null \
|
||||
|| nmcli connection up "$con" >/dev/null
|
||||
}
|
||||
|
||||
if ! activate_connection "$CON" "$IFACE"; then
|
||||
if [ "$BOOT" = "1" ]; then
|
||||
echo "bt-netconfig: activation timed out (boot); profile saved, NM will retry" >&2
|
||||
exit 0
|
||||
fi
|
||||
err "connection activation failed or timed out after ${ACTIVATE_TIMEOUT}s"
|
||||
fi
|
||||
|
||||
# Brief settle, then report what NM actually applied.
|
||||
|
||||
@@ -2,17 +2,21 @@
|
||||
#
|
||||
# bt-netconfig-boot - re-apply saved network settings after reboot.
|
||||
# Runs as root from buttontask-network.service once NetworkManager is up.
|
||||
# Does not block UI startup: short waits, bounded activation, best-effort apply.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="${BUTTONTASK_ROOT:-/opt/buttontask}"
|
||||
CFG="$ROOT/config/network.json"
|
||||
SCRIPT="$ROOT/scripts/bt-netconfig"
|
||||
NM_WAIT_SEC="${BUTTONTASK_NET_NM_WAIT:-15}"
|
||||
IFACE_WAIT_SEC="${BUTTONTASK_NET_IFACE_WAIT:-10}"
|
||||
ACTIVATE_TIMEOUT="${BUTTONTASK_NET_ACTIVATE_TIMEOUT:-10}"
|
||||
|
||||
log() { echo "bt-netconfig-boot: $*"; }
|
||||
|
||||
wait_for_iface() {
|
||||
local iface="$1" i
|
||||
for i in $(seq 1 45); do
|
||||
for i in $(seq 1 "$IFACE_WAIT_SEC"); do
|
||||
ip link show "$iface" &>/dev/null && return 0
|
||||
sleep 1
|
||||
done
|
||||
@@ -21,19 +25,33 @@ wait_for_iface() {
|
||||
|
||||
wait_for_nm() {
|
||||
local i
|
||||
for i in $(seq 1 30); do
|
||||
for i in $(seq 1 "$NM_WAIT_SEC"); do
|
||||
nmcli general status &>/dev/null && return 0
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
activate_profile() {
|
||||
local con="$1" iface="$2"
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout "$ACTIVATE_TIMEOUT" nmcli connection up "$con" ifname "$iface" 2>/dev/null \
|
||||
|| timeout "$ACTIVATE_TIMEOUT" nmcli connection up "$con" 2>/dev/null \
|
||||
|| true
|
||||
else
|
||||
nmcli connection up "$con" ifname "$iface" 2>/dev/null \
|
||||
|| nmcli connection up "$con" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
apply_saved() {
|
||||
[ -f "$CFG" ] || { log "no saved config at $CFG"; return 0; }
|
||||
[ -x "$SCRIPT" ] || { log "bt-netconfig missing: $SCRIPT"; return 1; }
|
||||
|
||||
BUTTONTASK_NET_BOOT=1 \
|
||||
BUTTONTASK_NET_ACTIVATE_TIMEOUT="$ACTIVATE_TIMEOUT" \
|
||||
python3 - "$CFG" "$SCRIPT" <<'PY'
|
||||
import json, subprocess, sys
|
||||
import json, os, subprocess, sys
|
||||
|
||||
cfg_path, script = sys.argv[1], sys.argv[2]
|
||||
with open(cfg_path, encoding="utf-8") as f:
|
||||
@@ -50,7 +68,9 @@ if mode == "static":
|
||||
cfg.get("gateway", ""),
|
||||
cfg.get("dns", ""),
|
||||
]
|
||||
raise SystemExit(subprocess.run(args).returncode)
|
||||
env = os.environ.copy()
|
||||
env.setdefault("BUTTONTASK_NET_BOOT", "1")
|
||||
raise SystemExit(subprocess.run(args, env=env).returncode)
|
||||
PY
|
||||
}
|
||||
|
||||
@@ -63,13 +83,12 @@ wait_for_nm || log "NetworkManager not ready, continuing anyway"
|
||||
wait_for_iface "$IFACE" || log "interface $IFACE not found, continuing anyway"
|
||||
|
||||
if apply_saved; then
|
||||
log "network ready on $IFACE"
|
||||
log "network profile applied on $IFACE (activation may continue in background)"
|
||||
else
|
||||
log "saved config apply failed, trying buttontask profiles"
|
||||
while IFS= read -r con; do
|
||||
[ -z "$con" ] && continue
|
||||
iface="${con#buttontask-}"
|
||||
nmcli connection up "$con" ifname "$iface" 2>/dev/null \
|
||||
|| nmcli connection up "$con" 2>/dev/null || true
|
||||
activate_profile "$con" "$iface"
|
||||
done < <(nmcli -t -f NAME connection show 2>/dev/null | grep '^buttontask-' || true)
|
||||
fi
|
||||
|
||||
@@ -266,6 +266,45 @@ function syncResponseCheckUi() {
|
||||
if (adv) adv.classList.toggle("show", !!on);
|
||||
}
|
||||
|
||||
function fillLatchResetForm(lr) {
|
||||
const latchEl = $("#b-latch-enabled");
|
||||
if (!latchEl) return;
|
||||
const enabled = lr ? !!lr.enabled : true;
|
||||
latchEl.checked = enabled;
|
||||
$("#b-reset-url").value = lr?.resetUrl || "";
|
||||
$("#b-success-match").value = lr?.successMatch || "OK";
|
||||
const ffEl = $("#b-fire-and-forget");
|
||||
if (ffEl) ffEl.checked = !!lr?.fireAndForget;
|
||||
syncLatchUi();
|
||||
}
|
||||
|
||||
function readLatchResetFromForm() {
|
||||
if (!$("#b-latch-enabled")?.checked) {
|
||||
return { enabled: false };
|
||||
}
|
||||
return {
|
||||
enabled: true,
|
||||
resetUrl: $("#b-reset-url").value.trim(),
|
||||
successMatch: ($("#b-success-match").value.trim() || "OK"),
|
||||
fireAndForget: !!$("#b-fire-and-forget")?.checked,
|
||||
};
|
||||
}
|
||||
|
||||
function syncLatchUi() {
|
||||
const latchOn = $("#b-latch-enabled")?.checked;
|
||||
const fields = $("#b-latch-fields");
|
||||
const rcBlock = $("#b-response-check-block");
|
||||
const ffHint = document.querySelector(".latch-fire-hint");
|
||||
if (fields) fields.classList.toggle("show", !!latchOn);
|
||||
if (rcBlock) rcBlock.style.display = latchOn ? "none" : "";
|
||||
if (ffHint) {
|
||||
ffHint.style.display = latchOn && $("#b-fire-and-forget")?.checked ? "" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
$("#b-latch-enabled")?.addEventListener("change", syncLatchUi);
|
||||
$("#b-fire-and-forget")?.addEventListener("change", syncLatchUi);
|
||||
|
||||
$("#b-rc-enabled")?.addEventListener("change", syncResponseCheckUi);
|
||||
$("#b-rc-add-rule")?.addEventListener("click", () => {
|
||||
$("#b-rc-rules")?.appendChild(createRuleRow());
|
||||
@@ -284,14 +323,17 @@ function openDialogFor(btn) {
|
||||
$("#b-icon").value = btn?.iconPath || "";
|
||||
$("#b-type").value = btn?.action?.type || "http_get";
|
||||
$("#b-url").value = btn?.action?.url || "";
|
||||
$("#b-action-timeout").value = btn?.action?.timeoutMs ?? 7000;
|
||||
$("#b-success").value = btn?.feedback?.successText || "";
|
||||
$("#b-error").value = btn?.feedback?.errorText || "Ошибка";
|
||||
$("#b-pending").value = btn?.feedback?.pendingText ?? "...";
|
||||
$("#b-color").value = btn?.color || "#7b007b";
|
||||
$("#b-trigger-mode").value = btn?.trigger?.mode || "hold";
|
||||
$("#b-hold").value = btn?.trigger?.holdMs || 800;
|
||||
fillLatchResetForm(btn?.latchReset);
|
||||
fillResponseCheckForm(btn?.action?.responseCheck);
|
||||
syncHoldRow();
|
||||
syncLatchUi();
|
||||
dlg.showModal();
|
||||
}
|
||||
|
||||
@@ -322,18 +364,21 @@ form?.addEventListener("submit", async (e) => {
|
||||
if (e.submitter && e.submitter.value === "cancel") return;
|
||||
e.preventDefault();
|
||||
const id = $("#b-id").value;
|
||||
const latchReset = readLatchResetFromForm();
|
||||
const payload = {
|
||||
label: $("#b-label").value,
|
||||
iconPath: $("#b-icon").value,
|
||||
actionType: $("#b-type").value,
|
||||
url: $("#b-url").value,
|
||||
timeoutMs: parseInt($("#b-action-timeout").value, 10) || 7000,
|
||||
successText: $("#b-success").value,
|
||||
errorText: $("#b-error").value,
|
||||
pendingText: $("#b-pending").value || "...",
|
||||
color: $("#b-color").value,
|
||||
triggerMode: $("#b-trigger-mode").value,
|
||||
holdMs: parseInt($("#b-hold").value, 10),
|
||||
responseCheck: readResponseCheckFromForm(),
|
||||
latchReset,
|
||||
responseCheck: latchReset.enabled ? { enabled: false } : readResponseCheckFromForm(),
|
||||
};
|
||||
if (id) await api("PUT", `/api/buttons/${encodeURIComponent(id)}`, payload);
|
||||
else await api("POST", "/api/buttons", payload);
|
||||
|
||||
@@ -397,6 +397,9 @@ dialog h3 { margin: 0 0 6px; }
|
||||
dialog menu { display: flex; justify-content: flex-end; gap: 8px; padding: 0; margin: 14px 0 0; }
|
||||
|
||||
.response-check-block { margin-top: 6px; }
|
||||
.latch-reset-block { margin-top: 8px; }
|
||||
.latch-reset-fields { display: none; margin-top: 8px; gap: 8px; flex-direction: column; }
|
||||
.latch-reset-fields.show { display: flex; }
|
||||
.response-check-advanced { display: none; margin-top: 10px; gap: 10px; flex-direction: column; }
|
||||
.response-check-advanced.show { display: flex; }
|
||||
.response-rules-head {
|
||||
|
||||
@@ -411,7 +411,7 @@
|
||||
<p class="hint">Используется и для входа в веб-конфигуратор, и для настроек в приложении.</p>
|
||||
<div class="row">
|
||||
<label>Новый пароль
|
||||
<input type="password" id="new-password" placeholder="Оставьте пустым чтобы не менять">
|
||||
<input type="text" id="new-password" placeholder="Оставьте пустым чтобы не менять" autocomplete="off">
|
||||
</label>
|
||||
<button type="button" id="save-password" class="primary">Сохранить пароль</button>
|
||||
</div>
|
||||
@@ -440,6 +440,28 @@
|
||||
</select>
|
||||
</label>
|
||||
<label>URL <input type="text" id="b-url" required></label>
|
||||
<label>Ожидание ответа, мс
|
||||
<input type="number" id="b-action-timeout" min="1000" max="600000" step="500" value="7000">
|
||||
</label>
|
||||
<p class="hint b-action-timeout-hint">Таймаут одного HTTP-запроса. По умолчанию 7000 мс (7 с). При «Мгновенном отклике» влияет только на фоновый запрос и лог.</p>
|
||||
|
||||
<div class="latch-reset-block" id="b-latch-block">
|
||||
<label class="check">
|
||||
<input type="checkbox" id="b-latch-enabled" checked>
|
||||
Сброс повторным нажатием
|
||||
</label>
|
||||
<p class="hint">Первое нажатие — вызов по URL выше. Успех, если в ответе есть указанный текст или HTTP < 400. Зелёный статус держится до повторного нажатия (URL сброса).</p>
|
||||
<div id="b-latch-fields" class="latch-reset-fields">
|
||||
<label>URL сброса <input type="text" id="b-reset-url" placeholder="http://host/api/click/btn?bid=01*04*00"></label>
|
||||
<label>Успех если в ответе <input type="text" id="b-success-match" value="OK"></label>
|
||||
<label class="check">
|
||||
<input type="checkbox" id="b-fire-and-forget">
|
||||
Мгновенный отклик
|
||||
</label>
|
||||
<p class="hint latch-fire-hint">Статус обновляется сразу после нажатия; команда на сервер уходит параллельно.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label>Текст успеха <input type="text" id="b-success"></label>
|
||||
<label>Текст ошибки <input type="text" id="b-error" value="Ошибка"></label>
|
||||
<label>Текст ожидания <input type="text" id="b-pending" value="..."></label>
|
||||
|
||||
Reference in New Issue
Block a user