feat: индикатор присутствия в конфигурациях (иконка глаза)

Открытые конфигурации фиксируются в локальном SQLite (app_settings) и
передаются на сервер через qt_client_schema_state.open_config_uuids при
каждом цикле синхронизации. Списки конфигураций обогащаются полем viewers,
в таблицах отображается иконка глаза с подсказкой при наличии других
пользователей, открывших эту конфигурацию.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-06-30 00:56:06 +03:00
parent 9601619d1b
commit 50f0e4f76f
7 changed files with 208 additions and 4 deletions

View File

@@ -242,6 +242,7 @@ function renderConfigs(configs) {
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Цена (за 1 шт)</th>';
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Кол-во</th>';
html += '<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Сумма</th>';
html += '<th class="px-2 py-3 w-8"></th>';
html += '<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Действия</th>';
html += '</tr></thead><tbody class="divide-y">';
@@ -298,6 +299,16 @@ function renderConfigs(configs) {
html += '<td class="px-4 py-3 text-sm text-gray-500">' + pricePerUnit + '</td>';
html += '<td class="px-4 py-3 text-sm text-gray-500">' + serverCount + '</td>';
html += '<td class="px-4 py-3 text-sm text-right">' + total + '</td>';
const viewers = c.viewers || [];
if (viewers.length > 0) {
const names = viewers.map(escapeHtml).join(', ');
html += '<td class="px-2 py-3 text-center w-8">';
html += '<span title="Открыта: ' + names + '" class="inline-flex items-center justify-center text-blue-500 cursor-default">';
html += '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>';
html += '</span></td>';
} else {
html += '<td class="px-2 py-3 w-8"></td>';
}
html += '<td class="px-4 py-3 text-sm text-right space-x-2">';
if (configStatusMode === 'archived') {
html += '<button onclick="reactivateConfig(\'' + c.uuid + '\')" class="text-emerald-600 hover:text-emerald-800" title="Восстановить">';

View File

@@ -985,6 +985,16 @@ document.addEventListener('DOMContentLoaded', async function() {
if (configUUID) {
loadVendorSpec(configUUID);
}
// Presence: announce that this config is open and keep renewing every 4 min
if (configUUID) {
const sendPresence = () => fetch('/api/configs/' + configUUID + '/presence', {method: 'POST'}).catch(() => {});
sendPresence();
setInterval(sendPresence, 4 * 60 * 1000);
window.addEventListener('beforeunload', () => {
fetch('/api/configs/' + configUUID + '/presence', {method: 'DELETE', keepalive: true}).catch(() => {});
});
}
});
async function loadAllComponents() {

View File

@@ -518,7 +518,16 @@ function renderConfigs(configs) {
html += '<td class="px-4 py-3 text-sm text-gray-500"><input type="number" min="1" value="' + serverCount + '" class="w-16 px-1 py-0.5 border rounded text-center text-sm" data-uuid="' + c.uuid + '" data-prev="' + serverCount + '" onchange="updateConfigServerCount(this)"></td>';
}
html += '<td class="px-4 py-3 text-sm text-right" data-total-uuid="' + c.uuid + '">' + formatMoneyNoDecimals(total) + '</td>';
html += '<td class="px-2 py-3 text-sm text-center text-gray-500 w-12">main</td>';
const projViewers = c.viewers || [];
if (projViewers.length > 0) {
const projNames = projViewers.map(escapeHtml).join(', ');
html += '<td class="px-2 py-3 text-center w-12">';
html += '<span title="Открыта: ' + projNames + '" class="inline-flex items-center justify-center text-blue-500 cursor-default">';
html += '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>';
html += '</span></td>';
} else {
html += '<td class="px-2 py-3 w-12"></td>';
}
html += '<td class="px-4 py-3 text-sm text-right whitespace-nowrap"><div class="inline-flex items-center justify-end gap-2">';
if (configStatusMode === 'archived') {
html += '<button onclick="reactivateConfig(\'' + c.uuid + '\')" class="text-emerald-600 hover:text-emerald-800" title="Восстановить">';