diff --git a/backend/Dockerfile b/backend/Dockerfile index 32a7eed..ef04ec6 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,10 +1,11 @@ -FROM node:18-alpine +FROM node:22-alpine WORKDIR /app COPY package*.json ./ -RUN npm install --production +RUN apk add --no-cache python3 make g++ \ + && npm install --production COPY . . @@ -12,4 +13,4 @@ RUN mkdir -p /app/data EXPOSE 3000 -CMD ["node", "server.js"] \ No newline at end of file +CMD ["node", "server.js"] diff --git a/backend/server.js b/backend/server.js index 2c4b588..f9d46d3 100644 --- a/backend/server.js +++ b/backend/server.js @@ -5,6 +5,7 @@ const { v4: uuidv4 } = require('uuid'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); +const os = require('os'); const app = express(); const PORT = process.env.PORT || 3000; @@ -6000,6 +6001,22 @@ app.get('/health', (req, res) => { startAutomationWorker(); +function logRuntimeInfo() { + let osPretty = ''; + try { + const raw = fs.readFileSync('/etc/os-release', 'utf8'); + const match = raw.match(/^PRETTY_NAME="?(.*?)"?$/m); + if (match && match[1]) { + osPretty = match[1]; + } + } catch (error) { + // ignore + } + const osInfo = osPretty || `${os.platform()} ${os.release()}`; + console.log(`Runtime: Node ${process.version}, OS ${osInfo}`); +} + app.listen(PORT, '0.0.0.0', () => { + logRuntimeInfo(); console.log(`Server running on port ${PORT}`); }); diff --git a/web/automation.js b/web/automation.js index 5b28fdb..2dbc9b7 100644 --- a/web/automation.js +++ b/web/automation.js @@ -86,6 +86,7 @@ let sortState = { key: 'next', dir: 'asc' }; let listInstance = null; let sse = null; + let relativeTimer = null; function toDateTimeLocal(value) { if (!value) return ''; @@ -509,6 +510,20 @@ `; } + function updateRelativeTimes() { + renderHero(); + if (!requestTableBody || !state.requests.length) return; + const byId = new Map(state.requests.map((req) => [String(req.id), req])); + requestTableBody.querySelectorAll('tr[data-id]').forEach((row) => { + const req = byId.get(row.dataset.id); + if (!req) return; + const nextEl = row.querySelector('.next'); + if (nextEl) nextEl.textContent = formatRelative(req.next_run_at); + const lastEl = row.querySelector('.last'); + if (lastEl) lastEl.textContent = formatRelative(req.last_run_at); + }); + } + function renderRequests() { if (!requestTableBody) return; requestTableBody.innerHTML = ''; @@ -946,10 +961,7 @@ function initListInstance() { const container = document.getElementById('automationTable'); if (!container) return; - if (listInstance) { - listInstance.remove(); - listInstance = null; - } + listInstance = null; const options = { listClass: 'list', valueNames: [ @@ -968,7 +980,6 @@ applyFilters(); listInstance.sort(sortState.key, { order: sortState.dir === 'desc' ? 'desc' : 'asc' }); updateSortIndicators(); - listInstance.on('updated', updateSortIndicators); } function applyFilters() { @@ -996,6 +1007,7 @@ return matchName && matchNext && matchLast && matchStatus && matchRuns; }); + updateSortIndicators(); } function getSelectedRequest() { @@ -1135,6 +1147,15 @@ applyPresetDisabling(); resetForm(); loadRequests(); + if (!relativeTimer) { + updateRelativeTimes(); + relativeTimer = setInterval(updateRelativeTimes, 60000); + document.addEventListener('visibilitychange', () => { + if (!document.hidden) { + updateRelativeTimes(); + } + }); + } form.addEventListener('submit', handleSubmit); intervalPreset.addEventListener('change', applyPresetDisabling);