3 Commits

Author SHA1 Message Date
zeroday 4d01a65859 docs: реестр операторов и privacy@ в уведомлении о куки
Приведение всех четырёх сайтов ООО к единому виду. Полноценная политика здесь
по-прежнему не нужна — форм и сбора данных на сайте нет, — но реквизиты
дополнены записью в реестре операторов (рег. номер 35-26-021324, приказ № 127
от 07.08.2026) и единым адресом privacy@umbyte.ru для вопросов по обработке.
2026-08-26 11:00:13 +03:00
zeroday 0dbbc7487e feat(landing): уведомление о куки и баннер
Баннер сделан по образцу АгроТО, но БЕЗ выбора категорий. На umbyte.ru нет
ни аналитики, ни рекламных пикселей, ни форм — только техническая сессия
админки. Окно настроек с переключателями, которые ничего не переключают,
показывало бы пользователю выбор, которого нет. Две кнопки: «Хорошо» и
«Отклонить», выбор помнится 180 дней.

Рядом /cookies.html — короткое уведомление: что за куки, откуда, на какой
срок, как отказаться, и прямо сказано, что персональные данные посетителей
сайт не собирает (кнопка связи открывает почтовый клиент, форм нет). Поэтому
полноценная политика обработки ПДн здесь не нужна — в отношении посетителей
компания не выступает оператором.

Если появится Метрика или форма обратной связи — берём полный вариант с
категориями из АгроТО, механика хранения совпадает.
2026-08-25 22:40:34 +03:00
zeroday f8730d9d09 fix: тип запроса секций и догоняющая миграция английских колонок
Оба дефекта вскрылись сборкой образа после слияния веток.

1. public.ts: SQL выбирает content_json_en, а описание типа запроса пришло из
   ветки визиток и этой колонки не знало — tsc отказывался собирать. След
   слияния: запрос из одной ветки, тип из другой.

2. Английские колонки (sections.content_json_en, products.title_en/subtitle_en/
   description_en) существуют на боевой базе, но их не создаёт ни одна
   миграция — когда-то добавили руками. На бою работало, на любой ЧИСТОЙ базе
   приложение упало бы на первом запросе /api/content. Заведена 005 с
   IF NOT EXISTS: на бою пусто, на чистой базе создаст.

Проверено на живом сайте: образ собран, контейнер healthy, миграции 004 и 005
применились сами, /api/content отдаёт 4 раздела на двух языках, 2 продукта,
полные реквизиты; страница визитки отвечает 200.
2026-08-25 22:35:20 +03:00
5 changed files with 211 additions and 1 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ export const publicRouter = Router();
publicRouter.get('/content', async (_req, res) => { publicRouter.get('/content', async (_req, res) => {
try { try {
const [sections, products, approach, settings] = await Promise.all([ const [sections, products, approach, settings] = await Promise.all([
query<{ key: string; title: string; content_json: unknown; sort_order: number }>( query<{ key: string; title: string; content_json: unknown; content_json_en: unknown; sort_order: number }>(
`SELECT key, title, content_json, content_json_en, sort_order `SELECT key, title, content_json, content_json_en, sort_order
FROM sections FROM sections
WHERE is_active = true WHERE is_active = true
+16
View File
@@ -0,0 +1,16 @@
-- Английские колонки для переключателя языка.
--
-- Колонки уже существуют на боевой базе — их когда-то добавили вручную, минуя
-- миграции. Найдено 25.08.2026 при слиянии веток: код их читает, а ни один
-- файл в db/ не создаёт, то есть на любой ЧИСТОЙ базе (новый стенд, локальная
-- разработка, восстановление из пустого) приложение падало бы на первом же
-- запросе /api/content.
--
-- IF NOT EXISTS делает миграцию безопасной для боевой базы: там она ничего не
-- изменит, только зафиксирует факт в _migrations.
ALTER TABLE sections ADD COLUMN IF NOT EXISTS content_json_en JSONB;
ALTER TABLE products ADD COLUMN IF NOT EXISTS title_en VARCHAR(256);
ALTER TABLE products ADD COLUMN IF NOT EXISTS subtitle_en VARCHAR(512);
ALTER TABLE products ADD COLUMN IF NOT EXISTS description_en TEXT;
+1
View File
@@ -17,5 +17,6 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
<script src="/cookies.js" defer></script>
</body> </body>
</html> </html>
+82
View File
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Уведомление о файлах cookie — Умный Байт</title>
<meta name="robots" content="index, follow">
<style>
:root { --ink:#0f172a; --muted:#475569; --line:#e2e8f0; }
*{box-sizing:border-box}
body{margin:0;background:#f8fafc;color:var(--ink);
font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
line-height:1.6;padding:32px 20px}
.wrap{max-width:720px;margin:0 auto;background:#fff;border:1px solid var(--line);
border-radius:16px;padding:36px}
h1{font-size:1.6rem;margin:0 0 6px;letter-spacing:-.02em}
.sub{color:var(--muted);font-size:14px;margin:0 0 28px}
h2{font-size:1.05rem;margin:28px 0 8px}
p,li{color:var(--muted);font-size:15px}
table{width:100%;border-collapse:collapse;margin-top:10px;font-size:14px}
th,td{text-align:left;padding:9px 10px;border-bottom:1px solid var(--line);vertical-align:top}
th{color:var(--ink);font-weight:600}
.back{display:inline-block;margin-top:28px;font-size:14px;color:var(--ink)}
.req{margin-top:28px;padding-top:20px;border-top:1px solid var(--line);font-size:13px;color:var(--muted)}
</style>
</head>
<body>
<div class="wrap">
<h1>Уведомление о файлах cookie</h1>
<p class="sub">Сайт umbyte.ru. Обновлено 25 августа 2026 года.</p>
<p>Куки — небольшие файлы, которые сайт сохраняет в браузере. Ниже честно
описано, какие из них использует этот сайт и зачем.</p>
<h2>Что мы используем</h2>
<p>Только технические куки. На сайте <strong>нет систем веб-аналитики</strong>
(ни Яндекс Метрики, ни Google Analytics), <strong>нет рекламных пикселей</strong>
и виджетов сторонних сервисов, которые следили бы за посетителями.</p>
<table>
<tr><th>Назначение</th><th>Когда появляется</th><th>Срок</th></tr>
<tr>
<td>Сессия входа в панель управления сайтом</td>
<td>Только у сотрудников после входа в закрытый раздел</td>
<td>До выхода или окончания срока сессии</td>
</tr>
<tr>
<td>Запоминание выбора в уведомлении о куки</td>
<td>После нажатия кнопки в баннере</td>
<td>180 дней</td>
</tr>
</table>
<h2>Персональные данные</h2>
<p>Через сайт мы не собираем персональные данные посетителей: форм обратной
связи и подписки здесь нет. Кнопка связи открывает ваш почтовый клиент —
письмо уходит напрямую, минуя наш сервер. Поэтому в отношении посетителей
сайта мы не выступаем оператором персональных данных.</p>
<h2>Как отказаться</h2>
<p>Нажмите «Отклонить» в баннере. Кроме того, куки можно удалить или запретить
в настройках браузера — в разделе конфиденциальности. Учтите, что при полном
запрете технических куки вход в закрытый раздел работать не будет.</p>
<h2>Изменения</h2>
<p>Если на сайте появятся аналитика или формы сбора данных, это уведомление
будет дополнено, а в баннере появится выбор категорий куки.</p>
<div class="req">
ООО «Умный Байт», ИНН 3528347856, КПП 352801001, ОГРН 1263500003667.<br>
162620, Вологодская область, город Череповец, улица Центральная, дом 12.<br>
Регистрационный номер в реестре операторов персональных данных: 35-26-021324
(приказ Роскомнадзора № 127 от 07.08.2026).<br>
Вопросы по обработке данных: <a href="mailto:privacy@umbyte.ru">privacy@umbyte.ru</a>
</div>
<a class="back" href="/">← На главную</a>
</div>
</body>
</html>
+111
View File
@@ -0,0 +1,111 @@
/* Куки-баннер сайта «Умный Байт».
*
* Сделан по образцу баннера АгроТО (там он живёт в public/landing/cookies.js),
* но без выбора категорий: на umbyte.ru нет ни аналитики, ни рекламных
* пикселей, ни форм — только техническая сессия админки. Городить окно
* настроек с переключателями, которые ничего не переключают, честнее не
* делать: пользователю показывают выбор, которого нет.
*
* Если на сайте появится Метрика или форма обратной связи — берём полный
* вариант с категориями из АгроТО, механика хранения совпадает.
*
* Хранение: localStorage["umbyte_cookie_consent_v1"] = {accepted, ts}
* Программно: window.umbyteCookies.show() / .get() / .reset()
*/
(function () {
'use strict';
var STORAGE_KEY = 'umbyte_cookie_consent_v1';
var TTL_DAYS = 180;
function read() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
var p = JSON.parse(raw);
if (!p || !p.ts) return null;
if ((Date.now() - new Date(p.ts).getTime()) / 86400000 > TTL_DAYS) return null;
return p;
} catch (e) { return null; }
}
function write(accepted) {
var data = { accepted: !!accepted, ts: new Date().toISOString() };
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch (e) {}
return data;
}
var css = [
'.umb-cc{position:fixed;inset:auto 0 0 0;z-index:9999;display:flex;justify-content:center;padding:16px;pointer-events:none;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}',
'.umb-cc-bar{pointer-events:auto;background:#fff;border:1px solid #e2e8f0;border-radius:14px;box-shadow:0 12px 40px rgba(15,23,42,.12);padding:16px 20px;max-width:860px;width:100%;display:flex;gap:16px;align-items:center;animation:umbUp .25s ease-out}',
'@keyframes umbUp{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}',
'.umb-cc-text{flex:1;margin:0;font-size:13.5px;line-height:1.5;color:#475569}',
'.umb-cc-text a{color:#0f172a;text-decoration:underline;text-underline-offset:2px}',
'.umb-cc-actions{display:flex;gap:8px;flex-shrink:0}',
'.umb-cc-btn{font-family:inherit;font-size:13px;font-weight:500;padding:9px 18px;border-radius:9px;border:1.5px solid #e2e8f0;background:#fff;color:#475569;cursor:pointer;transition:all .15s;white-space:nowrap}',
'.umb-cc-btn:hover{border-color:#94a3b8;color:#0f172a}',
'.umb-cc-btn-primary{background:#0f172a;color:#fff;border-color:#0f172a}',
'.umb-cc-btn-primary:hover{background:#1e293b;border-color:#1e293b;color:#fff}',
/* На телефоне: узкие отступы, мелкий шрифт, кнопки в один ряд с переносом
текста внутри — иначе длинная подпись обрезается многоточием. */
'@media(max-width:640px){',
'.umb-cc{padding:8px}',
'.umb-cc-bar{flex-direction:column;align-items:stretch;gap:10px;padding:12px 14px;border-radius:12px}',
'.umb-cc-text{font-size:12.5px;line-height:1.4}',
'.umb-cc-actions{width:100%;gap:6px}',
'.umb-cc-btn{flex:1;padding:8px 6px;font-size:11.5px;white-space:normal;line-height:1.2;display:flex;align-items:center;justify-content:center}',
'}'
].join('');
function injectCss() {
if (document.getElementById('umb-cc-style')) return;
var s = document.createElement('style');
s.id = 'umb-cc-style';
s.textContent = css;
document.head.appendChild(s);
}
function show() {
if (document.getElementById('umb-cc-bar')) return;
injectCss();
var el = document.createElement('div');
el.id = 'umb-cc-bar';
el.className = 'umb-cc';
el.innerHTML = ''
+ '<div class="umb-cc-bar" role="dialog" aria-label="Файлы cookie">'
+ '<p class="umb-cc-text">Мы используем только технические куки — они нужны, чтобы сайт работал. '
+ 'Аналитики и рекламных сервисов на сайте нет. Подробнее в <a href="/cookies.html">уведомлении о куки</a>.</p>'
+ '<div class="umb-cc-actions">'
+ '<button type="button" class="umb-cc-btn" data-act="no">Отклонить</button>'
+ '<button type="button" class="umb-cc-btn umb-cc-btn-primary" data-act="yes">Хорошо</button>'
+ '</div>'
+ '</div>';
el.addEventListener('click', function (e) {
var act = e.target && e.target.getAttribute && e.target.getAttribute('data-act');
if (!act) return;
write(act === 'yes');
var b = document.getElementById('umb-cc-bar');
if (b) b.remove();
});
document.body.appendChild(el);
}
window.umbyteCookies = {
show: show,
get: read,
reset: function () {
try { localStorage.removeItem(STORAGE_KEY); } catch (e) {}
var b = document.getElementById('umb-cc-bar');
if (b) b.remove();
show();
}
};
function init() { if (!read()) show(); }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();