Files
Button/webconfig/templates/index.html
T

504 lines
26 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ButtonTask — конфигуратор</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js" defer></script>
<script src="{{ url_for('static', filename='app.js') }}" defer></script>
</head>
<body>
<header>
<div class="brand">
<span class="logo"></span>
<div>
<h1>ButtonTask</h1>
<span class="meta">Веб-конфигуратор · версия {{ version.version }}</span>
</div>
</div>
<a href="{{ url_for('auth.logout') }}" class="logout"><button type="button" class="ghost">Выйти</button></a>
</header>
<nav class="tabs" role="tablist">
<button class="tab active" data-tab="buttons">Кнопки</button>
<button class="tab" data-tab="appearance">Внешний вид</button>
<button class="tab" data-tab="network">Сеть</button>
<button class="tab" data-tab="system">Система</button>
<button class="tab" data-tab="update">Обновление</button>
<button class="tab" data-tab="security">Безопасность</button>
</nav>
<main>
<!-- ============ Кнопки ============ -->
<section class="panel active" id="tab-buttons">
<div class="card">
<div class="card-head">
<h2>Кнопки</h2>
<button id="add-btn" class="primary">+ Добавить</button>
</div>
<p class="hint">Перетаскивайте за «≡» для изменения порядка.</p>
<ul id="buttons-list" class="buttons-list">
{% for b in config.buttons %}
<li data-id="{{ b.id }}">
<span class="handle"></span>
<img class="btn-icon" src="{{ url_for('api.serve_icon', fname=b.iconPath) if b.iconPath else '' }}" alt="">
<div class="btn-info">
<strong>{{ b.label }}</strong>
<small>{{ b.action.type }}: {{ b.action.url }}</small>
</div>
<span class="swatch" style="background: {{ b.color or '#7b007b' }}"></span>
<button class="edit icon-only" data-id="{{ b.id }}" title="Редактировать"></button>
<button class="del icon-only" data-id="{{ b.id }}" title="Удалить"></button>
</li>
{% endfor %}
</ul>
{% if not config.buttons %}
<p class="empty">Кнопок пока нет. Нажмите «Добавить».</p>
{% endif %}
</div>
</section>
<!-- ============ Внешний вид ============ -->
<section class="panel" id="tab-appearance">
<div class="card">
<h2>Расположение</h2>
<div class="grid-form">
<label>Режим
<select id="layout-mode">
<option value="grid" {% if config.layout.mode == 'grid' %}selected{% endif %}>Сетка</option>
<option value="list" {% if config.layout.mode == 'list' %}selected{% endif %}>Список</option>
</select>
</label>
<label>Колонок
<input type="number" id="layout-columns" min="1" max="8" value="{{ config.layout.columns }}">
</label>
<label>Отступ
<input type="number" id="layout-spacing" min="0" max="40" value="{{ config.layout.spacing }}">
</label>
<label class="check">
<input type="checkbox" id="layout-show-labels" {% if config.layout.showLabels %}checked{% endif %}>
Показывать подписи
</label>
<label>Цвет подписей
<input type="color" id="layout-label-color" value="{{ config.layout.labelColor|default('#ffffff', true) }}">
</label>
</div>
<div class="card-actions">
<button id="save-layout" class="primary">Сохранить расположение</button>
</div>
</div>
<div class="card">
<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>
<option value="solid" {% if config.background.type == 'solid' %}selected{% endif %}>Цвет</option>
<option value="image" {% if config.background.type == 'image' %}selected{% endif %}>Картинка</option>
</select>
</label>
<label class="bg-solid-row bg-gradient-row">Основной цвет
<input type="color" id="bg-c1" value="{{ config.background.color1 }}">
</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 %}
<option value="{{ ic }}" {% if config.background.imagePath == ic %}selected{% endif %}>{{ ic }}</option>
{% endfor %}
</select>
</label>
</div>
<div class="card-actions compact">
<button id="save-bg" class="primary">Сохранить фон</button>
</div>
<hr class="section-divider">
<h3 class="subhead">Обводка при нажатии</h3>
<p class="hint">Цвет и ширина свечения вокруг кнопки при успехе, ошибке, удержании и ожидании ответа.</p>
<div class="feedback-panel">
<div class="feedback-section">
<h4 class="feedback-section-title">Общее</h4>
<label class="label-range">
<span class="label-caption">Радиус свечения: <span id="fb-glow-val">{{ config.feedback.glowRadius }}</span> px</span>
<input type="range" id="fb-glow" min="0" max="60" value="{{ config.feedback.glowRadius }}">
</label>
</div>
<div class="feedback-section">
<h4 class="feedback-section-title">Успех</h4>
<div class="feedback-section-grid">
<div class="grid-form feedback-section-form">
<label>Цвет
<input type="color" id="fb-ok-color" value="{{ config.feedback.okColor }}">
</label>
<label class="label-range">
<span class="label-caption">Ширина: <span id="fb-ok-width-val">{{ config.feedback.okWidth }}</span> px</span>
<input type="range" id="fb-ok-width" min="0" max="30" value="{{ config.feedback.okWidth }}">
</label>
</div>
<div class="feedback-section-preview">
<div class="preview-dot" id="fb-preview-ok">OK</div>
</div>
</div>
</div>
<div class="feedback-section">
<h4 class="feedback-section-title">Ошибка</h4>
<div class="feedback-section-grid">
<div class="grid-form feedback-section-form">
<label>Цвет
<input type="color" id="fb-error-color" value="{{ config.feedback.errorColor }}">
</label>
<label class="label-range">
<span class="label-caption">Ширина: <span id="fb-error-width-val">{{ config.feedback.errorWidth }}</span> px</span>
<input type="range" id="fb-error-width" min="0" max="30" value="{{ config.feedback.errorWidth }}">
</label>
</div>
<div class="feedback-section-preview">
<div class="preview-dot" id="fb-preview-error">ERR</div>
</div>
</div>
</div>
<div class="feedback-section">
<h4 class="feedback-section-title">Удержание</h4>
<div class="feedback-section-grid">
<div class="grid-form feedback-section-form">
<label>Цвет кольца прогресса
<input type="color" id="fb-hold-color" value="{{ config.feedback.holdColor|default('#ffffff', true) }}">
</label>
</div>
<div class="feedback-section-preview">
<div class="preview-dot" id="fb-preview-hold">HOLD</div>
</div>
</div>
</div>
<div class="feedback-section">
<h4 class="feedback-section-title">Ожидание</h4>
<div class="feedback-section-grid">
<div class="grid-form feedback-section-form">
<label>Цвет
<input type="color" id="fb-pending-color" value="{{ config.feedback.pendingColor|default('#ffffff', true) }}">
</label>
<label class="label-range">
<span class="label-caption">Ширина: <span id="fb-pending-width-val">{{ config.feedback.pendingWidth|default(6, true) }}</span> px</span>
<input type="range" id="fb-pending-width" min="0" max="30" value="{{ config.feedback.pendingWidth|default(6, true) }}">
</label>
</div>
<div class="feedback-section-preview">
<div class="preview-dot" id="fb-preview-pending"></div>
</div>
</div>
</div>
</div>
<div class="card-actions compact">
<button id="save-feedback" class="primary">Сохранить обводку</button>
</div>
</div>
<div class="card">
<h2>Иконки</h2>
<div class="row">
<input type="file" id="icon-file" accept=".png,.jpg,.jpeg,.bmp,.svg,.gif">
<button id="upload-icon">Загрузить</button>
</div>
<div id="icon-grid" class="icon-grid">
{% for ic in icons %}
<div class="icon-tile" data-name="{{ ic }}">
<img src="{{ url_for('api.serve_icon', fname=ic) }}" alt="{{ ic }}">
<span>{{ ic }}</span>
<button class="del icon-only" data-name="{{ ic }}"></button>
</div>
{% endfor %}
</div>
</div>
</section>
<!-- ============ Сеть ============ -->
<section class="panel" id="tab-network">
<div class="card">
<div class="card-head">
<h2>Ethernet</h2>
<button id="net-refresh" class="ghost">↻ Обновить</button>
</div>
<div id="net-status" class="net-status">Загрузка состояния…</div>
</div>
<div class="card">
<h2>Настройка интерфейса</h2>
<p class="warn">Изменение настроек может разорвать текущее соединение и сменить IP-адрес устройства.</p>
<div class="grid-form">
<label>Интерфейс
<select id="net-iface"></select>
</label>
<label>Режим
<select id="net-mode">
<option value="dhcp">DHCP (автоматически)</option>
<option value="static">Статический IP</option>
</select>
</label>
<label class="static-only">IP-адрес <input type="text" id="net-address" placeholder="192.168.1.50"></label>
<label class="static-only">Маска (префикс) <input type="number" id="net-prefix" min="1" max="32" value="24"></label>
<label class="static-only">Шлюз <input type="text" id="net-gateway" placeholder="192.168.1.1"></label>
<label class="static-only">DNS <input type="text" id="net-dns" placeholder="8.8.8.8, 1.1.1.1"></label>
</div>
<div class="card-actions">
<button id="net-apply" class="primary">Применить</button>
</div>
</div>
</section>
<!-- ============ Система ============ -->
<section class="panel" id="tab-system">
<div class="card">
<div class="card-head">
<h2>Состояние устройства</h2>
<button id="sys-refresh" class="ghost">↻ Обновить</button>
</div>
<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>
<div id="sys-clock" class="sys-clock">Текущее время: …</div>
<div class="row">
<label>Установить
<input type="datetime-local" id="sys-datetime" step="1">
</label>
<button id="sys-set-time" class="primary">Применить</button>
</div>
</div>
<div class="card">
<h2>Перезагрузка</h2>
<p class="warn">Полная перезагрузка устройства. Используйте только если что-то зависло.</p>
<button id="sys-reboot" class="del">Перезагрузить устройство</button>
</div>
<div class="card">
<div class="card-head">
<h2>Доступ к устройству</h2>
<button id="access-refresh" class="ghost">↻ Обновить</button>
</div>
<p class="hint">ICMP управляет ответом на ping. Изменение веб-порта перезапустит веб-сервис.</p>
<div id="access-status" class="access-status">Загрузка…</div>
<div class="access-grid">
<div class="access-row">
<span>Ping / ICMP</span>
<button id="icmp-on">Включить</button>
<button id="icmp-off" class="del">Отключить</button>
</div>
<div class="access-row">
<span>SSH</span>
<button id="ssh-on">Включить</button>
<button id="ssh-off" class="del">Отключить</button>
</div>
<label>Порт SSH
<div class="row">
<input type="number" id="ssh-port" min="1" max="65535" value="22">
<button id="save-ssh-port">Сохранить</button>
</div>
</label>
<label>Порт веб-конфигуратора
<div class="row">
<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>
<div class="card">
<h2>Журнал нажатий</h2>
<p class="hint">Какая кнопка нажата, куда ушёл запрос и чем завершился.</p>
<div class="log-wrap">
<table class="req-log" id="sys-log">
<thead>
<tr>
<th>Время</th>
<th>Кнопка</th>
<th>Метод</th>
<th>URL</th>
<th>Результат</th>
<th>мс</th>
</tr>
</thead>
<tbody id="sys-log-body">
<tr><td colspan="6" class="empty-cell">Загрузка…</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- ============ Обновление ============ -->
<section class="panel" id="tab-update">
<div class="update-layout">
<aside class="version-sidebar card">
<div class="card-head">
<h2>Версии</h2>
<button id="versions-refresh" class="ghost" title="Обновить"></button>
</div>
<p class="hint">Установленные на устройстве. Активную и версию для отката удалить нельзя.</p>
<ul id="version-list" class="version-list">
<li class="empty">Загрузка…</li>
</ul>
</aside>
<div class="update-main">
<div class="card">
<h2>Текущая версия</h2>
<div id="update-current" class="kv">
<div><span>Версия</span><strong id="update-version">{{ version.version }}</strong></div>
<div><span>Путь</span><code id="update-path">{{ version.path }}</code></div>
</div>
</div>
<div class="card">
<h2>Установить обновление</h2>
<p class="hint">Загрузите пакет <code>.tar.gz</code> (бинарь ButtonTask + webconfig + manifest.json). После применения выполняется проверка работоспособности; при ошибке запуска произойдёт автоматический откат.</p>
<div class="row">
<input type="file" id="update-file" accept=".tar.gz,.tgz">
<button id="update-upload" class="primary">Загрузить и применить</button>
</div>
<div id="update-status" class="update-status">Статус: проверка…</div>
<div class="card-actions">
<button id="update-rollback" class="del">Откатить на предыдущую версию</button>
</div>
</div>
</div>
</div>
</section>
<!-- ============ Безопасность ============ -->
<section class="panel" id="tab-security">
<div class="card">
<h2>Пароль</h2>
<p class="hint">Используется и для входа в веб-конфигуратор, и для настроек в приложении.</p>
<div class="row">
<label>Новый пароль
<input type="password" id="new-password" placeholder="Оставьте пустым чтобы не менять">
</label>
<button type="button" id="save-password" class="primary">Сохранить пароль</button>
</div>
</div>
</section>
</main>
<!-- Edit dialog -->
<dialog id="btn-dialog">
<form method="dialog" id="btn-form">
<h3 id="btn-dialog-title">Кнопка</h3>
<input type="hidden" id="b-id">
<label>Подпись <input type="text" id="b-label" required></label>
<label>Иконка
<select id="b-icon">
<option value="">— нет —</option>
{% for ic in icons %}
<option value="{{ ic }}">{{ ic }}</option>
{% endfor %}
</select>
</label>
<label>Метод
<select id="b-type">
<option value="http_get">http_get</option>
<option value="http_post">http_post</option>
</select>
</label>
<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">
<option value="hold">Удержание</option>
<option value="click">Клик</option>
</select>
</label>
<label id="b-hold-row">Длительность удержания: <span id="b-hold-val">800</span> мс
<input type="range" id="b-hold" min="200" max="3000" step="50" value="800">
</label>
<div class="response-check-block" id="b-response-check-block">
<label class="check">
<input type="checkbox" id="b-rc-enabled">
Расширенная проверка ответа (JSON)
</label>
<div id="b-rc-advanced" class="response-check-advanced">
<p class="hint">Пример ответа: <code>{"success":"ok"}</code>. Без этого блока проверяется только HTTP-код. В колонке результата правила выберите <strong>Ошибка</strong> / <strong>Ожидание</strong> / <strong>Принято</strong> — не оставляйте «Принято» для условий ошибки.</p>
<label class="check">
<input type="checkbox" id="b-rc-require-http" checked>
Требовать HTTP &lt; 400
</label>
<label>Если ни одно правило не подошло
<select id="b-rc-default">
<option value="error">Ошибка</option>
<option value="ok">Принято</option>
<option value="pending">Ожидание</option>
</select>
</label>
<div class="response-rules-head">
<strong>Правила</strong>
<button type="button" id="b-rc-add-rule" class="ghost">+ Правило</button>
</div>
<div id="b-rc-rules"></div>
<h4 class="subhead">Polling при «Ожидание»</h4>
<div class="grid-form response-check-poll">
<label>Интервал, мс
<input type="number" id="b-rc-poll-interval" min="500" max="60000" value="3000">
</label>
<label>Макс. попыток
<input type="number" id="b-rc-poll-attempts" min="1" max="100" value="20">
</label>
<label>Общий таймаут, мс
<input type="number" id="b-rc-poll-timeout" min="1000" max="600000" value="120000">
</label>
</div>
</div>
</div>
<menu>
<button value="cancel">Отмена</button>
<button id="b-save" value="ok" type="submit" class="primary">Сохранить</button>
</menu>
</form>
</dialog>
</body>
</html>