fix(shorts): плановая очистка, бэкап, часовой пояс и экранирование

- purgeScheduledDeletions() теперь чистит short_messages по purge_at
- short_messages добавлена в BACKUP_TABLES/BACKUP_SEQUENCE_TABLES,
  normalizeRestoreData и restore (поле автора проверяется на живость)
- сид short_messages_retention_days перенесён и в db/init.sql
- used_at по умолчанию берётся в зоне приложения (appTodayIso),
  а не по UTC
- поле «Использовано» скрыто при создании, открыто при правке
- удалён пустой escInline и мёртвые data-message/canEdit: сырой
  текст сообщения попадал в атрибут неэкранированным
This commit is contained in:
dev
2026-10-06 00:06:29 +03:00
parent ee09958644
commit d00de2cb1a
6 changed files with 73 additions and 19 deletions
+8 -10
View File
@@ -58,21 +58,20 @@ async function loadShorts() {
const dateStr = r.lesson_date ? fmtDateOnlyIso(r.lesson_date) : '';
const usedDateStr = r.used_at ? fmtDateOnlyIso(r.used_at) : '—';
const author = r.author_name || r.author_username || '—';
const canEdit = true;
return `
<tr data-edit-row data-message="${escInline(r.message || '')}">
<tr data-edit-row>
<td>${esc(r.resident)}</td>
<td style="white-space:nowrap">${dateStr}</td>
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.topic || '')}">${esc(r.topic || '')}</td>
<td style="max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.message || '')}">${esc(r.message || '')}</td>
<td style="white-space:nowrap">${esc(author)}</td>
<td style="white-space:nowrap">
<input type="checkbox" class="used-check" data-id="${r.id}" ${r.used ? 'checked' : ''} title="Использовано" ${canEdit ? '' : 'disabled'}>
<input type="checkbox" class="used-check" data-id="${r.id}" ${r.used ? 'checked' : ''} title="Использовано">
<div style="font-size:.72rem;color:var(--muted)">${r.used ? esc(usedDateStr) : '—'}</div>
</td>
<td>
${canEdit ? `<button class="row-btn" data-edit="${r.id}" title="Редактировать"><i data-lucide="pencil"></i></button>
<button class="row-btn del" data-del="${r.id}" title="Удалить (мягко)"><i data-lucide="trash-2"></i></button>` : '—'}
<button class="row-btn" data-edit="${r.id}" title="Редактировать"><i data-lucide="pencil"></i></button>
<button class="row-btn del" data-del="${r.id}" title="Удалить (мягко)"><i data-lucide="trash-2"></i></button>
</td>
</tr>`;
}).join('');
@@ -129,11 +128,14 @@ function openShortsModal(row) {
editingId = row ? row.id : null;
document.getElementById('shortsModalTitle').textContent = row ? `Сообщение #${row.id}` : 'Новое сообщение';
document.getElementById('shortsLinesHint').textContent = row ? ' (только текст сообщения)' : ' (формат: ФИО - текст или ФИО — текст)';
document.getElementById('shortsDate').value = row ? row.lesson_date : '';
document.getElementById('shortsDate').value = row ? row.lesson_date : todayIso();
document.getElementById('shortsTopic').value = row ? (row.topic || '') : '';
document.getElementById('shortsResident').value = row ? (row.resident || '') : '';
document.getElementById('shortsUsed').checked = row ? !!row.used : false;
document.getElementById('shortsUsedAt').value = row && row.used_at ? String(row.used_at).slice(0, 10) : '';
const usedDisplay = row ? '' : 'none';
document.getElementById('shortsUsedField').style.display = usedDisplay;
document.getElementById('shortsUsedAtField').style.display = usedDisplay;
if (row) {
document.getElementById('shortsLines').value = row.message || '';
} else {
@@ -317,8 +319,4 @@ function acceptShortsAi() {
closeShortsAi();
}
function escInline(s) {
return String(s || '').replace(/'/g, "'").replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
}
init();
+2 -2
View File
@@ -72,11 +72,11 @@
<label>Резидент (для добавления без разбора строк)</label>
<input type="text" id="shortsResident" class="settings-input" placeholder="Иванов И.И.">
</div>
<div class="settings-field" style="flex-direction:row;align-items:center;gap:8px">
<div class="settings-field" id="shortsUsedField" style="flex-direction:row;align-items:center;gap:8px;display:none">
<input type="checkbox" id="shortsUsed">
<label style="margin:0">Использовано</label>
</div>
<div class="settings-field">
<div class="settings-field" id="shortsUsedAtField" style="display:none">
<label>Дата использования</label>
<input type="date" id="shortsUsedAt" class="settings-input">
</div>