- Move service account credentials to .env (loaded server-side only) - server.js: inject credentials in proxy, strip any client-provided creds, replace deprecated url.parse with new URL - auth.js / dashboard.js: remove all hardcoded passwords from client code - dashboard.js: remove broken category filter, fix redundant user.info call (use stored userId), add HTML escaping against XSS - login.html: remove unused password field - dashboard.html: remove broken category filter UI - Add .gitignore to exclude .env and node_modules - Add .env.example as configuration template Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
352 lines
14 KiB
JavaScript
352 lines
14 KiB
JavaScript
function escapeHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function handleEdit(jobId) {
|
|
window.location.href = `editor.html?jobId=${encodeURIComponent(jobId)}`;
|
|
}
|
|
|
|
async function handlePDF(jobId) {
|
|
const jobCard = document.querySelector(`[data-job-id="${jobId}"]`);
|
|
const pdfBtn = jobCard ? jobCard.querySelector('.btn-pdf') : null;
|
|
|
|
if (pdfBtn) {
|
|
pdfBtn.disabled = true;
|
|
pdfBtn.textContent = 'Downloading...';
|
|
}
|
|
|
|
showPdfPopup();
|
|
|
|
try {
|
|
const job = allJobs.find(j => j.id === jobId);
|
|
if (!job) throw new Error('Job not found');
|
|
|
|
const authConfigStr = sessionStorage.getItem('authConfig');
|
|
const externSessionId = sessionStorage.getItem('externSessionId');
|
|
|
|
if (!authConfigStr || !externSessionId) {
|
|
sessionStorage.clear();
|
|
window.location.href = 'login.html';
|
|
return;
|
|
}
|
|
|
|
const authConfig = JSON.parse(authConfigStr);
|
|
|
|
const params = new URLSearchParams({
|
|
command: 'job.export.pdf',
|
|
authDomain: 'local',
|
|
sessionId: externSessionId,
|
|
clientId: '7',
|
|
id: jobId,
|
|
result: 'file',
|
|
});
|
|
|
|
const response = await fetch(`${authConfig.apiBaseUrl}?${params}`);
|
|
if (!response.ok) throw new Error(`Failed to generate PDF (${response.status})`);
|
|
|
|
const blob = await response.blob();
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = blobUrl;
|
|
link.download = `${job.title || job.documentName || 'document'}_${jobId}.pdf`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(blobUrl);
|
|
} catch (error) {
|
|
console.error('PDF export error:', error);
|
|
alert('Failed to export PDF. Please try again.');
|
|
} finally {
|
|
if (pdfBtn) {
|
|
pdfBtn.disabled = false;
|
|
pdfBtn.textContent = 'PDF';
|
|
}
|
|
}
|
|
}
|
|
|
|
function showPdfPopup() {
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'pdf-popup-overlay';
|
|
|
|
const popup = document.createElement('div');
|
|
popup.className = 'pdf-popup';
|
|
popup.innerHTML = `
|
|
<p class="pdf-popup-message">PDF download requested</p>
|
|
<button class="pdf-popup-ok-btn">OK</button>
|
|
`;
|
|
|
|
overlay.appendChild(popup);
|
|
document.body.appendChild(overlay);
|
|
requestAnimationFrame(() => overlay.classList.add('active'));
|
|
|
|
const close = () => {
|
|
overlay.classList.remove('active');
|
|
setTimeout(() => overlay.remove(), 200);
|
|
};
|
|
popup.querySelector('.pdf-popup-ok-btn').addEventListener('click', close);
|
|
overlay.addEventListener('click', e => { if (e.target === overlay) close(); });
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
checkAuthentication();
|
|
loadJobsFromAPI();
|
|
setupEventListeners();
|
|
});
|
|
|
|
function checkAuthentication() {
|
|
if (sessionStorage.getItem('isAuthenticated') !== 'true') {
|
|
window.location.href = 'login.html';
|
|
}
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
document.getElementById('refreshBtn').addEventListener('click', loadJobsFromAPI);
|
|
document.getElementById('signOutBtn').addEventListener('click', handleSignOut);
|
|
}
|
|
|
|
async function handleSignOut() {
|
|
const signOutBtn = document.getElementById('signOutBtn');
|
|
signOutBtn.disabled = true;
|
|
signOutBtn.textContent = 'Signing out...';
|
|
|
|
try {
|
|
const externSessionId = sessionStorage.getItem('externSessionId');
|
|
const authConfigStr = sessionStorage.getItem('authConfig');
|
|
|
|
if (externSessionId && authConfigStr) {
|
|
const authConfig = JSON.parse(authConfigStr);
|
|
const params = new URLSearchParams({
|
|
command: 'user.session.extern.remove',
|
|
authDomain: 'local',
|
|
clientId: '7',
|
|
externSessionId,
|
|
});
|
|
await fetch(`${authConfig.apiBaseUrl}?${params}`, { headers: { Accept: 'text/xml' } });
|
|
}
|
|
} catch (error) {
|
|
console.error('Sign out error:', error);
|
|
} finally {
|
|
sessionStorage.clear();
|
|
window.location.href = 'login.html';
|
|
}
|
|
}
|
|
|
|
let allJobs = [];
|
|
let filteredJobs = [];
|
|
|
|
async function loadJobsFromAPI() {
|
|
const loadingMessage = document.getElementById('loadingMessage');
|
|
const errorMessage = document.getElementById('errorMessage');
|
|
const jobsGrid = document.getElementById('jobsGrid');
|
|
const emptyState = document.getElementById('emptyState');
|
|
|
|
loadingMessage.style.display = 'block';
|
|
errorMessage.style.display = 'none';
|
|
emptyState.style.display = 'none';
|
|
jobsGrid.innerHTML = '';
|
|
|
|
try {
|
|
const authConfigStr = sessionStorage.getItem('authConfig');
|
|
const userId = sessionStorage.getItem('userId');
|
|
const externSessionId = sessionStorage.getItem('externSessionId');
|
|
|
|
if (!authConfigStr || !userId || !externSessionId) {
|
|
sessionStorage.clear();
|
|
window.location.href = 'login.html';
|
|
return;
|
|
}
|
|
|
|
const authConfig = JSON.parse(authConfigStr);
|
|
|
|
const params = new URLSearchParams({
|
|
command: 'job.list',
|
|
authDomain: 'local',
|
|
clientId: '7',
|
|
userId,
|
|
includeDocumentInfos: '1',
|
|
includeDocumentPreviews: '1',
|
|
includeDocumentWorkflows: '1',
|
|
includeDocumentMetadata: '1',
|
|
});
|
|
params.append('status[0]', 'STARTED');
|
|
params.append('status[1]', 'RUNNING');
|
|
|
|
const response = await fetch(`${authConfig.apiBaseUrl}?${params}`, {
|
|
headers: { Accept: 'text/xml' },
|
|
});
|
|
const xmlDoc = new DOMParser().parseFromString(await response.text(), 'text/xml');
|
|
|
|
const errorNode = xmlDoc.querySelector('error');
|
|
if (errorNode) {
|
|
throw new Error(errorNode.querySelector('message')?.textContent || 'Failed to load jobs');
|
|
}
|
|
|
|
allJobs = Array.from(xmlDoc.querySelectorAll('job')).map(parseJobFromXML);
|
|
filteredJobs = allJobs;
|
|
|
|
loadingMessage.style.display = 'none';
|
|
|
|
if (allJobs.length === 0) {
|
|
emptyState.style.display = 'block';
|
|
} else {
|
|
renderJobs();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load jobs:', error);
|
|
loadingMessage.style.display = 'none';
|
|
errorMessage.textContent = error.message || 'Failed to load jobs. Please try again.';
|
|
errorMessage.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function parseJobFromXML(jobNode) {
|
|
const get = sel => jobNode.querySelector(sel)?.textContent || '';
|
|
|
|
const previewNode = jobNode.querySelector('document > preview');
|
|
const thumbnail = previewNode?.textContent
|
|
? `data:image/jpeg;base64,${previewNode.textContent}`
|
|
: null;
|
|
|
|
const documentVersion = get('document > version');
|
|
const documentName = get('document > name');
|
|
const jobName = get('name');
|
|
|
|
return {
|
|
id: get('id'),
|
|
documentId: get('document > id'),
|
|
title: documentVersion || documentName || jobName || 'Untitled',
|
|
documentName: documentVersion || documentName || 'N/A',
|
|
language: get('translationLanguage > name') || 'N/A',
|
|
pages: get('document > pages') || 'N/A',
|
|
status: get('status'),
|
|
isOpened: get('isOpened') === 'true',
|
|
totalItems: parseInt(get('totalItems')) || 0,
|
|
doneItems: parseInt(get('doneItems')) || 0,
|
|
finishItems: parseInt(get('finishItems')) || 0,
|
|
lastUserName: get('lastuser > name') || 'N/A',
|
|
lastEdit: get('lastedit') || 'N/A',
|
|
thumbnail: thumbnail || 'https://via.placeholder.com/300x400/cccccc/666666?text=No+Preview',
|
|
};
|
|
}
|
|
|
|
function updateOverallProgress() {
|
|
let totalFinishItems = 0;
|
|
let totalDoneItems = 0;
|
|
let totalAllItems = 0;
|
|
|
|
allJobs.forEach(job => {
|
|
totalFinishItems += job.finishItems;
|
|
totalDoneItems += job.doneItems;
|
|
totalAllItems += job.totalItems;
|
|
});
|
|
|
|
const finishPercent = totalAllItems > 0 ? (totalFinishItems / totalAllItems * 100) : 0;
|
|
const donePercent = totalAllItems > 0 ? (totalDoneItems / totalAllItems * 100) : 0;
|
|
const completedItems = totalFinishItems + totalDoneItems;
|
|
const overallPercent = totalAllItems > 0 ? Math.round((completedItems / totalAllItems) * 100) : 0;
|
|
|
|
document.getElementById('overallProgressText').innerHTML =
|
|
`<strong>${completedItems} of ${totalAllItems} elements done (${overallPercent}%)</strong>`;
|
|
document.getElementById('overallDone').style.width = `${donePercent}%`;
|
|
document.getElementById('overallFinish').style.width = `${finishPercent}%`;
|
|
document.getElementById('overallRemaining').style.width = `${100 - finishPercent - donePercent}%`;
|
|
document.getElementById('overallDoneText').textContent = `Done: ${totalDoneItems} (${Math.round(donePercent)}%)`;
|
|
document.getElementById('overallFinishText').textContent = `Finished: ${totalFinishItems} (${Math.round(finishPercent)}%)`;
|
|
document.getElementById('overallTotalText').textContent = `Total: ${totalAllItems}`;
|
|
}
|
|
|
|
function renderJobs() {
|
|
const jobsGrid = document.getElementById('jobsGrid');
|
|
const emptyState = document.getElementById('emptyState');
|
|
|
|
jobsGrid.innerHTML = '';
|
|
|
|
if (filteredJobs.length === 0) {
|
|
emptyState.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
emptyState.style.display = 'none';
|
|
filteredJobs.forEach(job => jobsGrid.appendChild(createJobCard(job)));
|
|
updateOverallProgress();
|
|
}
|
|
|
|
function createJobCard(job) {
|
|
const card = document.createElement('div');
|
|
card.className = 'job-card';
|
|
card.setAttribute('data-job-id', job.id);
|
|
|
|
let statusClass = 'status-ready';
|
|
const statusLower = job.status.toLowerCase();
|
|
if (statusLower === 'running') statusClass = 'status-progress';
|
|
else if (statusLower === 'finish') statusClass = 'status-done';
|
|
|
|
const lockedClass = job.isOpened ? ' job-locked' : '';
|
|
const disabledAttr = job.isOpened ? ' disabled' : '';
|
|
const lockIcon = job.isOpened ? '<div class="lock-icon">🔒</div>' : '';
|
|
|
|
const finishPercent = job.totalItems > 0 ? (job.finishItems / job.totalItems * 100) : 0;
|
|
const donePercent = job.totalItems > 0 ? (job.doneItems / job.totalItems * 100) : 0;
|
|
const completedItems = job.finishItems + job.doneItems;
|
|
const overallPercent = job.totalItems > 0 ? Math.round((completedItems / job.totalItems) * 100) : 0;
|
|
|
|
const title = escapeHtml(job.title);
|
|
const documentName = escapeHtml(job.documentName);
|
|
const documentId = escapeHtml(job.documentId);
|
|
const language = escapeHtml(job.language);
|
|
const pages = escapeHtml(job.pages);
|
|
const lastUserName = escapeHtml(job.lastUserName);
|
|
const lastEdit = escapeHtml(job.lastEdit);
|
|
const jobId = escapeHtml(job.id);
|
|
const thumbnail = escapeHtml(job.thumbnail);
|
|
|
|
card.innerHTML = `
|
|
<div class="job-card-header">${title}${lockIcon}</div>
|
|
<div class="job-preview">
|
|
<img src="${thumbnail}" alt="${title}" onerror="this.src='https://via.placeholder.com/300x400/cccccc/666666?text=No+Preview'">
|
|
</div>
|
|
<div class="job-details">
|
|
<p class="job-document-name">${documentName}</p>
|
|
<p class="job-info-line"><strong>Document ID:</strong> ${documentId}</p>
|
|
<p class="job-info-line"><strong>Language:</strong> ${language}</p>
|
|
<p class="job-info-line"><strong>Pages:</strong> ${pages}</p>
|
|
<p class="job-info-line"><strong>Last User:</strong> ${lastUserName}</p>
|
|
<p class="job-info-line"><strong>Last Edit:</strong> ${lastEdit}</p>
|
|
|
|
<p class="job-info-line"><strong>${completedItems} of ${job.totalItems} elements done (${overallPercent}%)</strong></p>
|
|
|
|
<div class="progress-bar-container">
|
|
<div class="progress-bar">
|
|
<div class="progress-segment progress-done" style="width: ${donePercent}%"></div>
|
|
<div class="progress-segment progress-finish" style="width: ${finishPercent}%"></div>
|
|
<div class="progress-segment progress-remaining" style="width: ${100 - finishPercent - donePercent}%"></div>
|
|
</div>
|
|
<div class="progress-legend">
|
|
<span class="legend-item">
|
|
<span class="legend-color legend-done"></span>
|
|
Done: ${job.doneItems} (${Math.round(donePercent)}%)
|
|
</span>
|
|
<span class="legend-item">
|
|
<span class="legend-color legend-finish"></span>
|
|
Finished: ${job.finishItems} (${Math.round(finishPercent)}%)
|
|
</span>
|
|
<span class="legend-item">
|
|
<span class="legend-color legend-remaining"></span>
|
|
Total: ${job.totalItems}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="job-actions">
|
|
<button class="btn-edit${lockedClass}"${disabledAttr} onclick="handleEdit('${jobId}')">Edit</button>
|
|
<button class="btn-pdf${lockedClass}"${disabledAttr} onclick="handlePDF('${jobId}')">PDF</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return card;
|
|
}
|