added backlight and fixed outbound

This commit is contained in:
2026-07-21 09:17:48 +03:00
parent ff2b8a250e
commit 5392273794
37 changed files with 1560 additions and 76 deletions
+7
View File
@@ -275,6 +275,13 @@ def api_set_settings():
if not isinstance(val, str) or not val.strip():
abort(400, "password required")
val = val.strip()
if k == "webPort":
try:
val = int(val)
except (TypeError, ValueError):
abort(400, "invalid web port")
if val < 1024 or val > 65535:
abort(400, "web port must be 102465535")
st[k] = val
cfg["settings"] = st
save_config(cfg)
+4 -1
View File
@@ -18,7 +18,7 @@ from api import api_bp
from auth import auth_bp, load_or_create_secret, login_required
from config_store import list_icons, load_config
from network import network_bp
from system_info import record_request, system_bp
from system_info import apply_saved_brightness, record_request, system_bp
from updater import current_version, updater_bp
@@ -38,6 +38,9 @@ def create_app():
app.register_blueprint(updater_bp)
app.register_blueprint(system_bp)
# Restore LCD brightness from config (best-effort; needs bt-brightness + sudoers).
apply_saved_brightness()
@app.before_request
def _request_start():
g._req_start = time.time()
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
#
# bt-brightness - LCD backlight control for ButtonTask.
#
# Usage:
# bt-brightness status
# bt-brightness set <0-100>
#
# Invoked through sudo by the web configurator / Qt UI.
# Prefers /sys/class/backlight/backlight; otherwise the first device with
# max_brightness > 0.
set -euo pipefail
err() { echo "bt-brightness: $*" >&2; exit 1; }
BACKLIGHT_ROOT="/sys/class/backlight"
find_device() {
local d preferred="$BACKLIGHT_ROOT/backlight" max
if [ -d "$preferred" ] && [ -f "$preferred/max_brightness" ]; then
max="$(cat "$preferred/max_brightness" 2>/dev/null || echo 0)"
if [ "$max" -gt 0 ] 2>/dev/null; then
echo "$preferred"
return 0
fi
fi
for d in "$BACKLIGHT_ROOT"/*; do
[ -d "$d" ] || continue
[ -f "$d/max_brightness" ] || continue
max="$(cat "$d/max_brightness" 2>/dev/null || echo 0)"
if [ "$max" -gt 0 ] 2>/dev/null; then
echo "$d"
return 0
fi
done
return 1
}
emit_status() {
local dev="$1" max cur percent
max="$(cat "$dev/max_brightness")"
if [ -f "$dev/actual_brightness" ]; then
cur="$(cat "$dev/actual_brightness")"
else
cur="$(cat "$dev/brightness")"
fi
if [ "$max" -gt 0 ]; then
percent=$(( (cur * 100 + max / 2) / max ))
else
percent=0
fi
[ "$percent" -gt 100 ] && percent=100
[ "$percent" -lt 0 ] && percent=0
printf '{"device":"%s","brightness":%s,"max":%s,"percent":%s}\n' \
"$(basename "$dev")" "$cur" "$max" "$percent"
}
set_percent() {
local percent="$1" dev max value
[[ "$percent" =~ ^[0-9]+$ ]] || err "percent must be an integer 0-100"
[ "$percent" -ge 0 ] && [ "$percent" -le 100 ] || err "percent must be 0-100"
dev="$(find_device)" || err "no backlight device found"
max="$(cat "$dev/max_brightness")"
[ "$max" -gt 0 ] || err "max_brightness is 0"
value=$(( (percent * max + 50) / 100 ))
[ "$value" -lt 0 ] && value=0
[ "$value" -gt "$max" ] && value="$max"
# Avoid completely blank panel unless explicitly requested as 0.
printf '%s\n' "$value" > "$dev/brightness"
emit_status "$dev"
}
cmd_status() {
local dev
dev="$(find_device)" || err "no backlight device found"
emit_status "$dev"
}
case "${1:-}" in
status) cmd_status ;;
set) [ "$#" -eq 2 ] || err "usage: bt-brightness set <0-100>"; set_percent "$2" ;;
*) err "usage: bt-brightness {status|set <0-100>}" ;;
esac
+47 -4
View File
@@ -101,6 +101,37 @@ switch_to() {
ln -sfn "$1" "$CURRENT"
}
# Install privileged helpers + sudoers from the staged package so OTA can
# deliver new bt-* scripts (e.g. bt-brightness) without a full redeploy.
sync_helpers() {
local dest="$1"
local src="$dest/webconfig/scripts"
local s name tmp_sudoers
[ -d "$src" ] || { log "sync_helpers: no $src"; return 0; }
mkdir -p "$ROOT/scripts"
for s in "$src"/bt-*; do
[ -f "$s" ] || continue
name="$(basename "$s")"
install -m 0755 "$s" "$ROOT/scripts/$name"
chown root:root "$ROOT/scripts/$name" 2>/dev/null || true
sed -i 's/\r$//' "$ROOT/scripts/$name" 2>/dev/null || true
log "synced helper $name"
done
if [ -f "$src/buttontask.sudoers" ]; then
tmp_sudoers="$(mktemp)"
cp "$src/buttontask.sudoers" "$tmp_sudoers"
sed -i 's/\r$//' "$tmp_sudoers" 2>/dev/null || true
chmod 0440 "$tmp_sudoers"
if visudo -cf "$tmp_sudoers" >/dev/null 2>&1; then
install -m 0440 "$tmp_sudoers" /etc/sudoers.d/buttontask
log "synced sudoers"
else
log "sudoers validation failed — kept previous /etc/sudoers.d/buttontask"
fi
rm -f "$tmp_sudoers"
fi
}
cmd_stage() {
local pkg="$1"
[ -f "$pkg" ] || { write_status error "package not found"; exit 1; }
@@ -138,6 +169,9 @@ cmd_stage() {
chmod -R u=rwX,go=rX "$dest" 2>/dev/null || true
rm -f "$pkg"
write_status helpers "установка хелперов"
sync_helpers "$dest"
write_status switching "$ver"
switch_to "$dest"
restart_services
@@ -167,6 +201,14 @@ cmd_rollback() {
write_status rollback_done "откат выполнен"
}
cmd_sync_helpers() {
local dest
dest="$(readlink -f "$CURRENT" 2>/dev/null || true)"
[ -n "$dest" ] && [ -d "$dest" ] || { echo "no current version" >&2; exit 1; }
sync_helpers "$dest"
echo "helpers synced from $dest"
}
cmd_remove() {
local name="$1"
[[ "$name" =~ ^[A-Za-z0-9._-]+$ ]] || { echo "invalid version name" >&2; exit 1; }
@@ -184,8 +226,9 @@ cmd_remove() {
}
case "${1:-}" in
stage) [ "$#" -ge 2 ] || { echo "usage: bt-update stage <pkg>"; exit 2; }; cmd_stage "$2" ;;
rollback) cmd_rollback ;;
remove) [ "$#" -ge 2 ] || { echo "usage: bt-update remove <name>"; exit 2; }; cmd_remove "$2" ;;
*) echo "usage: bt-update {stage <pkg>|rollback|remove <name>}"; exit 2 ;;
stage) [ "$#" -ge 2 ] || { echo "usage: bt-update stage <pkg>"; exit 2; }; cmd_stage "$2" ;;
rollback) cmd_rollback ;;
sync-helpers) cmd_sync_helpers ;;
remove) [ "$#" -ge 2 ] || { echo "usage: bt-update remove <name>"; exit 2; }; cmd_remove "$2" ;;
*) echo "usage: bt-update {stage <pkg>|rollback|sync-helpers|remove <name>}"; exit 2 ;;
esac
+2
View File
@@ -9,6 +9,7 @@
# buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-settime
# buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-reboot
# buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-access
# buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-brightness
# EOF
# sudo chmod 0440 /etc/sudoers.d/buttontask
# sudo visudo -cf /etc/sudoers.d/buttontask # validate
@@ -23,3 +24,4 @@ buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-service
buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-settime
buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-reboot
buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-access
buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-brightness
+59 -1
View File
@@ -486,6 +486,34 @@ async function loadAccess() {
}
}
function renderBrightness(s) {
const el = $("#brightness-status");
const slider = $("#brightness-slider");
const val = $("#brightness-val");
if (!el) return;
if (!s || s.ok === false) {
el.innerHTML = `<p class="warn">${(s && s.error) || "подсветка недоступна"}</p>`;
return;
}
const pct = s.percent != null ? s.percent : 100;
el.textContent = `Сейчас: ${pct}%` +
(s.device ? ` (${s.device}: ${s.brightness}/${s.max})` : "") +
(s.saved != null ? ` · сохранено: ${s.saved}%` : "");
if (slider && !slider.dataset.touched) {
slider.value = pct;
if (val) val.textContent = pct + "%";
}
}
async function loadBrightness() {
try {
const s = await api("GET", "/api/system/brightness");
renderBrightness(s);
} catch (e) {
renderBrightness({ ok: false, error: e.message });
}
}
async function setAccess(action, value) {
const r = await api("POST", "/api/system/access", { action, value });
if (action !== "webPort") renderAccessStatus(r);
@@ -505,6 +533,7 @@ async function loadSystem() {
dtInput.value = stats.clock.replace(" ", "T").slice(0, 16);
}
loadAccess();
loadBrightness();
} catch (e) {
const el = $("#sys-stats");
if (el) el.textContent = e.message;
@@ -526,6 +555,31 @@ $("#web-port")?.addEventListener("input", (e) => {
e.target.dataset.touched = "1";
});
$("#brightness-slider")?.addEventListener("input", (e) => {
e.target.dataset.touched = "1";
const val = $("#brightness-val");
if (val) val.textContent = e.target.value + "%";
});
$("#brightness-refresh")?.addEventListener("click", () => {
const slider = $("#brightness-slider");
if (slider) delete slider.dataset.touched;
loadBrightness();
});
$("#brightness-save")?.addEventListener("click", async () => {
const percent = parseInt($("#brightness-slider")?.value, 10);
if (Number.isNaN(percent) || percent < 0 || percent > 100) {
flash("Некорректная яркость", true);
return;
}
try {
const s = await api("POST", "/api/system/brightness", { percent });
const slider = $("#brightness-slider");
if (slider) delete slider.dataset.touched;
renderBrightness(s);
flash(`Яркость: ${s.percent}%`);
} catch (e) { flash(e.message, true); }
});
$("#sys-set-time")?.addEventListener("click", async () => {
const raw = $("#sys-datetime")?.value;
if (!raw) { flash("Укажите дату и время", true); return; }
@@ -572,9 +626,13 @@ $("#save-ssh-port")?.addEventListener("click", async () => {
flash("Порт SSH обновлён");
} catch (e) { flash(e.message, true); }
});
const MIN_USER_PORT = 1024;
$("#save-web-port")?.addEventListener("click", async () => {
const port = parseInt($("#web-port").value, 10);
if (!port || port < 1 || port > 65535) { flash("Некорректный веб-порт", true); return; }
if (!port || port < MIN_USER_PORT || port > 65535) {
flash(`Веб-порт должен быть от ${MIN_USER_PORT} до 65535 (1–1023 зарезервированы в Linux)`, true);
return;
}
if (!confirm(`Сменить веб-порт на ${port}? Веб-сервис перезапустится.`)) return;
try {
await setAccess("webPort", port);
+92 -2
View File
@@ -200,13 +200,16 @@ def access_status():
return status
MIN_USER_PORT = 1024
def set_web_port(port):
try:
port = int(port)
except (TypeError, ValueError):
raise ValueError("invalid web port")
if port < 1 or port > 65535:
raise ValueError("invalid web port")
if port < MIN_USER_PORT or port > 65535:
raise ValueError(f"web port must be {MIN_USER_PORT}65535")
cfg = load_config()
cfg.setdefault("settings", {})["webPort"] = port
save_config(cfg)
@@ -219,6 +222,72 @@ def set_web_port(port):
return port
def _run_brightness(*args, timeout=10):
script = str(scripts_dir() / "bt-brightness")
proc = subprocess.run(
["sudo", "-n", script, *args],
capture_output=True,
text=True,
timeout=timeout,
check=False,
)
for line in reversed((proc.stdout or "").splitlines()):
line = line.strip()
if line.startswith("{") and line.endswith("}"):
return json.loads(line)
if proc.returncode != 0:
err = (proc.stderr or proc.stdout or "bt-brightness failed").strip()
raise RuntimeError(err)
raise RuntimeError((proc.stdout or "bt-brightness returned no status").strip())
def brightness_status():
live = _run_brightness("status")
cfg = load_config()
saved = cfg.get("settings", {}).get("brightness")
try:
saved = int(saved) if saved is not None else None
except (TypeError, ValueError):
saved = None
live["saved"] = saved
live["ok"] = True
return live
def set_brightness(percent):
try:
percent = int(percent)
except (TypeError, ValueError):
raise ValueError("percent must be an integer 0-100")
if percent < 0 or percent > 100:
raise ValueError("percent must be 0-100")
live = _run_brightness("set", str(percent))
cfg = load_config()
cfg.setdefault("settings", {})["brightness"] = percent
save_config(cfg)
live["saved"] = percent
live["ok"] = True
return live
def apply_saved_brightness():
"""Restore brightness from config at service start. No-op if unset."""
cfg = load_config()
saved = cfg.get("settings", {}).get("brightness")
if saved is None:
return None
try:
percent = int(saved)
except (TypeError, ValueError):
return None
if percent < 0 or percent > 100:
return None
try:
return _run_brightness("set", str(percent))
except Exception:
return None
@system_bp.route("/api/system/stats", methods=["GET"])
@login_required
def api_system_stats():
@@ -261,6 +330,27 @@ def api_access_set():
return jsonify({"ok": False, "error": str(e)}), 500
@system_bp.route("/api/system/brightness", methods=["GET"])
@login_required
def api_brightness_get():
try:
return jsonify(brightness_status())
except RuntimeError as e:
return jsonify({"ok": False, "error": str(e)}), 500
@system_bp.route("/api/system/brightness", methods=["POST"])
@login_required
def api_brightness_set():
data = request.get_json(force=True, silent=True) or {}
try:
return jsonify(set_brightness(data.get("percent")))
except ValueError as e:
abort(400, str(e))
except RuntimeError as e:
return jsonify({"ok": False, "error": str(e)}), 500
@system_bp.route("/api/system/time", methods=["GET"])
@login_required
def api_system_time_get():
+18 -1
View File
@@ -274,6 +274,22 @@
<div id="sys-stats" class="sys-stats">Загрузка…</div>
</div>
<div class="card">
<div class="card-head">
<h2>Яркость экрана</h2>
<button id="brightness-refresh" class="ghost">↻ Обновить</button>
</div>
<p class="hint">Подсветка LCD. Снижение яркости уменьшает нагрев. Значение сохраняется и применяется при старте.</p>
<div id="brightness-status" class="sys-clock">Загрузка…</div>
<div class="row">
<label style="flex:1">Яркость
<input type="range" id="brightness-slider" min="1" max="100" value="100">
</label>
<span id="brightness-val">100%</span>
<button id="brightness-save" class="primary">Применить</button>
</div>
</div>
<div class="card">
<h2>Дата и время</h2>
<p class="hint">Без сети NTP не работает — задайте время вручную. Сохраняется в системные часы и RTC (если есть).</p>
@@ -318,9 +334,10 @@
</label>
<label>Порт веб-конфигуратора
<div class="row">
<input type="number" id="web-port" min="1" max="65535" value="8080">
<input type="number" id="web-port" min="1024" max="65535" value="8080">
<button id="save-web-port">Сохранить и перезапустить веб</button>
</div>
<span class="hint">Порты 1–1023 зарезервированы в Linux и недоступны без root.</span>
</label>
</div>
</div>