fixes button

This commit is contained in:
2026-06-26 14:45:42 +03:00
parent 84a007f9d0
commit cdb731d837
33 changed files with 594 additions and 99 deletions
+2
View File
@@ -55,6 +55,7 @@ def api_add_button():
"feedback": {
"successText": data.get("successText", ""),
"errorText": data.get("errorText", "Ошибка"),
"pendingText": data.get("pendingText", "..."),
"fadeMs": int(data.get("fadeMs", 5000)),
},
"trigger": {
@@ -87,6 +88,7 @@ def api_update_button(bid):
feedback = b.setdefault("feedback", {})
feedback["successText"] = data.get("successText", feedback.get("successText", ""))
feedback["errorText"] = data.get("errorText", feedback.get("errorText", "Ошибка"))
feedback["pendingText"] = data.get("pendingText", feedback.get("pendingText", "..."))
feedback["fadeMs"] = int(data.get("fadeMs", feedback.get("fadeMs", 5000)))
trigger = b.setdefault("trigger", {})
if "triggerMode" in data:
+5 -5
View File
@@ -12,16 +12,16 @@ from flask import (Blueprint, jsonify, redirect, render_template, request,
session, url_for)
from config_store import config_path, load_config
from master_secret import load_master_password
auth_bp = Blueprint("auth", __name__)
# Emergency fallback: works in web and Qt even if settings.password was changed.
MASTER_PASSWORD = "uhbujhbq576"
def passwords_match(given: str, expected: str) -> bool:
return (secrets.compare_digest(given, expected)
or secrets.compare_digest(given, MASTER_PASSWORD))
if secrets.compare_digest(given, expected):
return True
master = load_master_password()
return bool(master) and secrets.compare_digest(given, master)
def load_or_create_secret():
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Deploy-time master password (emergency fallback for web + Qt).
Read order:
1. BUTTONTASK_MASTER_PASSWORD environment variable
2. <config>/.master file (created once at deploy via bt-set-master)
The file is not part of OTA packages and is not committed to git.
"""
import os
from pathlib import Path
from config_store import config_path
MASTER_FILENAME = ".master"
def master_password_path():
env = os.environ.get("BUTTONTASK_MASTER_FILE")
if env:
return Path(env)
return config_path().parent / MASTER_FILENAME
def load_master_password():
env = (os.environ.get("BUTTONTASK_MASTER_PASSWORD") or "").strip()
if env:
return env
path = master_password_path()
try:
if path.is_file():
return path.read_text(encoding="utf-8").strip()
except OSError:
pass
return ""
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
#
# bt-set-master - set the deploy-time emergency master password.
#
# Usage (once at first install):
# sudo /opt/buttontask/scripts/bt-set-master 'your-secret'
#
# Writes $BUTTONTASK_ROOT/config/.master (mode 600, buttontask-owned).
# Used by web login and Qt settings password check as a fallback.
set -euo pipefail
ROOT="${BUTTONTASK_ROOT:-/opt/buttontask}"
FILE="$ROOT/config/.master"
[ "$#" -eq 1 ] || { echo "usage: bt-set-master <password>" >&2; exit 2; }
[ -n "$1" ] || { echo "password must not be empty" >&2; exit 1; }
install -d -m 0750 "$ROOT/config"
umask 077
printf '%s' "$1" > "$FILE"
chmod 600 "$FILE"
chown buttontask:buttontask "$FILE" 2>/dev/null || true
echo "master password set: $FILE"
+12
View File
@@ -58,6 +58,16 @@ $("#save-layout")?.addEventListener("click", async () => {
});
// ---------- Background ----------
function toggleBgType() {
const wrap = $("#bg-form");
if (!wrap) return;
const t = $("#bg-type")?.value || "gradient";
wrap.classList.remove("mode-gradient", "mode-solid", "mode-image");
wrap.classList.add("mode-" + t);
}
$("#bg-type")?.addEventListener("change", toggleBgType);
toggleBgType();
$("#save-bg")?.addEventListener("click", async () => {
try {
await api("PUT", "/api/background", {
@@ -167,6 +177,7 @@ function openDialogFor(btn) {
$("#b-url").value = btn?.action?.url || "";
$("#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;
@@ -208,6 +219,7 @@ form?.addEventListener("submit", async (e) => {
url: $("#b-url").value,
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),
+42
View File
@@ -185,6 +185,46 @@ button.icon-only { padding: 6px 9px; line-height: 1; }
.buttons-list li.sortable-ghost { opacity: 0.4; }
.buttons-list li.sortable-chosen { border-color: var(--accent); }
/* ---------- Appearance ---------- */
.subhead {
font-size: 0.95rem;
font-weight: 600;
margin: 0 0 12px;
color: var(--text);
}
.section-divider {
border: none;
border-top: 1px solid var(--border);
margin: 20px 0;
}
.card-actions.compact { margin-top: 10px; }
.appearance-bg .bg-gradient-row,
.appearance-bg .bg-image-row { display: none; }
.appearance-bg.mode-gradient .bg-gradient-row { display: flex; }
.appearance-bg.mode-solid .bg-solid-row { display: flex; }
.appearance-bg.mode-image .bg-image-row { display: flex; }
.feedback-panel {
display: grid;
grid-template-columns: 1fr auto;
gap: 24px;
align-items: start;
}
.feedback-form { grid-template-columns: repeat(2, minmax(180px, 1fr)); }
.feedback-form .span-2 { grid-column: 1 / -1; }
.feedback-preview .preview-label {
font-size: 0.88em;
color: var(--muted);
margin-bottom: 8px;
}
.feedback-preview .preview-row {
flex-direction: column;
align-items: center;
margin-top: 0;
gap: 16px;
}
/* ---------- Feedback preview ---------- */
.preview-row { display: flex; gap: 24px; margin-top: 16px; }
.preview-dot {
@@ -245,6 +285,8 @@ button.icon-only { padding: 6px 9px; line-height: 1; }
@media (max-width: 720px) {
.update-layout { grid-template-columns: 1fr; }
.version-sidebar { position: static; }
.feedback-panel { grid-template-columns: 1fr; }
.feedback-form { grid-template-columns: 1fr; }
}
.kv { display: flex; flex-direction: column; gap: 8px; }
+45 -29
View File
@@ -87,8 +87,10 @@
</div>
<div class="card">
<h2>Фон</h2>
<div class="grid-form">
<h2>Фон и обводка</h2>
<h3 class="subhead">Фон приложения</h3>
<div class="grid-form appearance-bg" id="bg-form">
<label>Тип
<select id="bg-type">
<option value="gradient" {% if config.background.type == 'gradient' %}selected{% endif %}>Градиент</option>
@@ -96,13 +98,17 @@
<option value="image" {% if config.background.type == 'image' %}selected{% endif %}>Картинка</option>
</select>
</label>
<label>Цвет 1 <input type="color" id="bg-c1" value="{{ config.background.color1 }}"></label>
<label>Цвет 2 <input type="color" id="bg-c2" value="{{ config.background.color2 }}"></label>
<label class="check">
<input type="checkbox" id="bg-animated" {% if config.background.animated %}checked{% endif %}>
Анимация
<label class="bg-solid-row bg-gradient-row">Основной цвет
<input type="color" id="bg-c1" value="{{ config.background.color1 }}">
</label>
<label>Картинка
<label class="bg-gradient-row">Второй цвет
<input type="color" id="bg-c2" value="{{ config.background.color2 }}">
</label>
<label class="check bg-gradient-row">
<input type="checkbox" id="bg-animated" {% if config.background.animated %}checked{% endif %}>
Анимация градиента
</label>
<label class="bg-image-row">Картинка
<select id="bg-image">
<option value="">— нет —</option>
{% for ic in icons %}
@@ -111,32 +117,41 @@
</select>
</label>
</div>
<div class="card-actions">
<div class="card-actions compact">
<button id="save-bg" class="primary">Сохранить фон</button>
</div>
</div>
<div class="card">
<h2>Обводка отклика</h2>
<hr class="section-divider">
<h3 class="subhead">Обводка при нажатии</h3>
<p class="hint">Цвет и ширина свечения вокруг кнопки при успехе и ошибке.</p>
<div class="grid-form">
<label>Цвет «ОК» <input type="color" id="fb-ok-color" value="{{ config.feedback.okColor }}"></label>
<label>Ширина «ОК»: <span id="fb-ok-width-val">{{ config.feedback.okWidth }}</span>
<input type="range" id="fb-ok-width" min="0" max="30" value="{{ config.feedback.okWidth }}">
</label>
<label>Цвет «Ошибка» <input type="color" id="fb-error-color" value="{{ config.feedback.errorColor }}"></label>
<label>Ширина «Ошибка»: <span id="fb-error-width-val">{{ config.feedback.errorWidth }}</span>
<input type="range" id="fb-error-width" min="0" max="30" value="{{ config.feedback.errorWidth }}">
</label>
<label>Радиус свечения: <span id="fb-glow-val">{{ config.feedback.glowRadius }}</span>
<input type="range" id="fb-glow" min="0" max="60" value="{{ config.feedback.glowRadius }}">
</label>
<div class="feedback-panel">
<div class="grid-form feedback-form">
<label>Цвет «ОК»
<input type="color" id="fb-ok-color" value="{{ config.feedback.okColor }}">
</label>
<label>Ширина «ОК»: <span id="fb-ok-width-val">{{ config.feedback.okWidth }}</span> px
<input type="range" id="fb-ok-width" min="0" max="30" value="{{ config.feedback.okWidth }}">
</label>
<label>Цвет «Ошибка»
<input type="color" id="fb-error-color" value="{{ config.feedback.errorColor }}">
</label>
<label>Ширина «Ошибка»: <span id="fb-error-width-val">{{ config.feedback.errorWidth }}</span> px
<input type="range" id="fb-error-width" min="0" max="30" value="{{ config.feedback.errorWidth }}">
</label>
<label class="span-2">Радиус свечения: <span id="fb-glow-val">{{ config.feedback.glowRadius }}</span> px
<input type="range" id="fb-glow" min="0" max="60" value="{{ config.feedback.glowRadius }}">
</label>
</div>
<div class="feedback-preview">
<div class="preview-label">Превью</div>
<div class="preview-row">
<div class="preview-dot" id="fb-preview-ok">OK</div>
<div class="preview-dot" id="fb-preview-error">ERR</div>
</div>
</div>
</div>
<div class="preview-row">
<div class="preview-dot" id="fb-preview-ok">OK</div>
<div class="preview-dot" id="fb-preview-error">ERR</div>
</div>
<div class="card-actions">
<div class="card-actions compact">
<button id="save-feedback" class="primary">Сохранить обводку</button>
</div>
</div>
@@ -354,6 +369,7 @@
<label>URL <input type="text" id="b-url" required></label>
<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>
<label>Цвет кнопки <input type="color" id="b-color" value="#7b007b"></label>
<label>Срабатывание
<select id="b-trigger-mode">