intra_monitor/public/js/auth.js
2026-03-18 14:19:29 +03:00

233 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

async function doLogin() {
const username=document.getElementById('login-username').value.trim();
const password=document.getElementById('login-password').value;
const errEl =document.getElementById('login-error');
errEl.textContent='';
if(!username||!password){errEl.textContent='Заполните все поля';return;}
const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username,password})});
const data=await r.json();
if(!r.ok){errEl.textContent=data.error||'Ошибка входа';return;}
authToken =data.token;
currentUser={username:data.username,is_admin:data.is_admin};
localStorage.setItem('intradesk_token',authToken);
showApp(); init();
}
document.addEventListener('keydown',e=>{
if(e.key==='Enter'&&!document.getElementById('login-overlay').classList.contains('hidden')) doLogin();
});
function doLogout() {
localStorage.removeItem('intradesk_token');
authToken=null; currentUser=null;
if(statusPollTimer) clearInterval(statusPollTimer);
if(clientCountdownTimer) clearInterval(clientCountdownTimer);
stopSSE();
document.getElementById('app-shell').style.display='none';
document.getElementById('login-overlay').classList.remove('hidden');
document.getElementById('login-password').value='';
}
function showApp() {
document.getElementById('login-overlay').classList.add('hidden');
document.getElementById('app-shell').style.display='flex';
const u=currentUser.username;
const initials=avatarInitials(u);
const color=avatarColor(u);
const av=document.getElementById('sb-avatar');
if(av){av.textContent=initials;av.style.background=color;}
if(document.getElementById('sb-username')) document.getElementById('sb-username').textContent=u;
if(document.getElementById('sb-email')) document.getElementById('sb-email').textContent=u+'@cleverparks.io';
const ns=document.getElementById('nav-settings');
if(ns) ns.style.display=currentUser.is_admin?'flex':'none'; // flex = topbar-icon-btn
}
async function init() {
await Promise.all([loadMarks(),loadNotes(),loadMentions(),loadUserList(),loadCommentCounts()]);
await loadTasksFromServer();
if(currentUser.is_admin){
const s=await apiGet('/api/settings');
if(s) applySettings(s);
loadUsersTable();
}
const status=await apiGet('/api/status');
if(status){serverRefreshInterval=status.auto_refresh_interval||300;lastKnownUpdatedAt=status.last_updated;}
startSSE();
startStatusPolling();
}
async function loadMarks() {const d=await apiGet('/api/marks'); if(d) marksCache=d;}
async function loadNotes() {const d=await apiGet('/api/notes'); if(d) notesCache=d;}
async function loadUserList() {const d=await apiGet('/api/users/list'); if(d) knownUsers=d.map(u=>u.toLowerCase());}
async function loadCommentCounts(){const d=await apiGet('/api/comments/counts');if(d&&typeof d==='object')commentCounts=d;}
async function loadTasksFromServer(silent=false) {
if(!silent&&!allData.length) showLoading();
const data=await apiGet('/api/tasks');
if(!data) return;
allData=(data.tasks||[]).map(task=>{
const lc=extractLastComment(task);
// Восстанавливаем форсированный комментарий из кеша, если updatedat не изменился
if(!lc && forceComment) {
const tid=String(task.id);
const cached=forcedCommentCache.get(tid);
if(cached && cached.updatedat===task.updatedat) return {task,lastComment:cached.lastComment};
}
return {task,lastComment:lc};
});
lastKnownUpdatedAt=data.updated_at;
const el=id=>document.getElementById(id);
if(el('total-loaded')) el('total-loaded').textContent=allData.length;
if(el('total-count')) el('total-count').textContent=allData.length;
if(el('last-update')) el('last-update').textContent=data.updated_at?new Date(data.updated_at).toLocaleTimeString('ru-RU'):'—';
document.getElementById('update-banner')?.classList.remove('visible');
onTasksLoaded();
renderTable();
if(forceComment) fetchMissingComments();
}
async function fetchMissingComments() {
// Только задачи без комментария, для которых кеш отсутствует или устарел
const toFetch=allData.filter(i=>{
if(i.lastComment) return false;
const tid=String(i.task.id);
const cached=forcedCommentCache.get(tid);
return !cached || cached.updatedat!==i.task.updatedat;
});
if(!toFetch.length) return;
for(const item of toFetch) {
const tid=String(item.task.id);
const upd=encodeURIComponent(item.task.updatedat||'');
let lc=null;
try {
const res=await apiGet(`/api/tasks/lastcomment/${tid}?upd=${upd}`);
if(res?.entries?.length) {
const entry=res.entries[0];
const ev=(entry.events?.data||[]).find(e=>COMMENT_TYPES.has(e.type));
if(ev) lc={
username:entry.username, userid:entry.userid,
usertype:entry.usertype, eventat:entry.eventat,
text:ev.stringvalue||'', isPrivate:ev.type===55, eventType:ev.type
};
}
} catch {}
item.lastComment=lc;
// Кешируем результат (даже null) — повторный запрос только при изменении updatedat
forcedCommentCache.set(tid,{updatedat:item.task.updatedat,lastComment:lc});
}
renderTable();
}
async function loadTasks() {
const btn=document.getElementById('load-btn');
btn.disabled=true;btn.textContent='⏳ Загрузка...';
try {
const r=await apiPost('/api/tasks/refresh',{});
if(r?.error){showToast('❌ '+r.error,'warn');return;}
await Promise.all([loadMarks(),loadNotes()]);
await loadTasksFromServer(true);
} finally {btn.disabled=false;btn.textContent='↻ Обновить';}
}
async function loadAllTasks() {
const btn=document.getElementById('refresh-all-btn');
const status=document.getElementById('refresh-all-status');
if(btn){btn.disabled=true;btn.textContent='⏳ Загружаю все...';}
if(status) status.textContent='Может занять ~30 секунд...';
try {
const r=await apiPost('/api/tasks/refresh/full',{});
if(r?.error){showToast('❌ '+r.error,'warn');if(status)status.textContent='Ошибка: '+r.error;return;}
await Promise.all([loadMarks(),loadNotes()]);
await loadTasksFromServer(true);
if(status) status.textContent=`✓ Загружено ${r.count} заявок`;
showToast(`✓ Загружено ${r.count} заявок`,'success');
} finally {if(btn){btn.disabled=false;btn.textContent='🔄 Обновить ВСЕ заявки';}}
}
async function reloadData() {
await Promise.all([loadMarks(),loadNotes()]);
await loadTasksFromServer(true);
}
function startStatusPolling() {
if(statusPollTimer) clearInterval(statusPollTimer);
statusPollTimer=setInterval(async()=>{
const s=await apiGet('/api/status');
if(s?.last_updated && s.last_updated!==lastKnownUpdatedAt) {
lastKnownUpdatedAt=s.last_updated;
await Promise.all([loadMarks(),loadNotes()]);
await loadTasksFromServer(true);
}
},30000);
}
function applySettings(s) {
const f=id=>document.getElementById(id);
if(f('status-ids')) f('status-ids').value=s.status_ids||'';
if(f('event-types')) f('event-types').value=s.event_types||'';
if(f('server-refresh-interval'))f('server-refresh-interval').value=s.auto_refresh_interval||'300';
if(f('apikey-status')) f('apikey-status').textContent=s.api_key_set?'(ключ задан ✓)':'(не задан!)';
}
async function saveSettings() {
const body={status_ids:document.getElementById('status-ids').value.trim(),event_types:document.getElementById('event-types').value.trim(),auto_refresh_interval:document.getElementById('server-refresh-interval').value};
const apiKey=document.getElementById('api-key').value.trim();
if(apiKey) body.api_key=apiKey;
const r=await apiPost('/api/settings',body);
if(r?.ok){applySettings(body);document.getElementById('api-key').value='';showToast('✓ Настройки сохранены','success');}
else showToast('❌ '+(r?.error||'Ошибка'),'warn');
}
async function createUser() {
const username=document.getElementById('new-user-login').value.trim();
const password=document.getElementById('new-user-pass').value;
const msgEl =document.getElementById('user-msg');
const r=await apiPost('/api/auth/register',{username,password});
if(r?.ok){
msgEl.style.color='var(--green)';msgEl.textContent=`✓ Пользователь "${username}" создан`;
document.getElementById('new-user-login').value='';
document.getElementById('new-user-pass').value='';
loadUsersTable();loadUserList();
} else {msgEl.style.color='var(--red)';msgEl.textContent=r?.error||'Ошибка';}
}
async function loadUsersTable() {
const users=await apiGet('/api/users');
if(!users) return;
const tbody=document.getElementById('users-tbody');
if(!tbody) return;
tbody.innerHTML=users.map(u=>`
<tr>
<td style="font-weight:600;">${esc(u.username)}</td>
<td>${u.created_at?new Date(u.created_at).toLocaleDateString('ru-RU'):'—'}</td>
<td><input class="inline-pass" type="password" id="pass-${u.id}" placeholder="новый пароль">
<button class="btn btn-default" style="font-size:11px;padding:4px 10px;margin-left:6px;" onclick="changePassword(${u.id})">Сменить</button></td>
<td>${u.username!=='admin'?`<button class="btn btn-danger" style="font-size:11px;padding:4px 10px;" onclick="deleteUser(${u.id},'${esc(u.username)}')">Удалить</button>`:''}</td>
</tr>`).join('');
}
async function changePassword(id) {
const inp=document.getElementById(`pass-${id}`);
if(!inp||inp.value.length<6){showToast('Пароль минимум 6 символов','warn');return;}
const r=await apiPost(`/api/users/${id}/password`,{password:inp.value},'PUT');
if(r?.ok){inp.value='';showToast('✓ Пароль изменён','success');}
else showToast(r?.error||'Ошибка','warn');
}
async function deleteUser(id,username) {
if(!confirm(`Удалить пользователя "${username}"?`)) return;
const r=await apiDelete(`/api/users/${id}`);
if(r?.ok){loadUsersTable();loadUserList();}
else showToast(r?.error||'Ошибка','warn');
}
window.addEventListener('DOMContentLoaded',()=>{
if(authToken){
try{
const p=JSON.parse(atob(authToken.split('.')[1]));
if(p.exp*1000>Date.now()){
currentUser={username:p.username,is_admin:p.username==='admin'};
showApp();init();return;
}
}catch{}
localStorage.removeItem('intradesk_token');authToken=null;
}
document.getElementById('login-overlay').classList.remove('hidden');
});