'use strict'; const FOOTER_TEXT = '© 2026 Martinhal IT - Joao Vaz - Version 2.3'; // Dates are stored as ISO (YYYY-MM-DD) so they sort correctly, but are always // shown to people as DD-MM-YYYY. function fmtDate(value) { if (!value) return ''; const m = String(value).match(/^(\d{4})-(\d{2})-(\d{2})/); return m ? `${m[3]}-${m[2]}-${m[1]}` : String(value); } function fmtDateTime(value) { if (!value) return ''; const str = String(value).replace('T', ' ').replace('Z', ''); const m = str.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ ](\d{2}:\d{2})(:\d{2})?)?/); if (!m) return str; const date = `${m[3]}-${m[2]}-${m[1]}`; return m[4] ? `${date} ${m[4]}${m[5] || ''}` : date; } // Accepts DD-MM-YYYY (what people type) or YYYY-MM-DD (native date inputs) // and returns ISO for the API, or '' when the value is not a real date. function toIsoDate(value) { const v = String(value || '').trim(); if (!v) return ''; let iso = ''; const dmy = v.match(/^(\d{2})-(\d{2})-(\d{4})$/); if (dmy) iso = `${dmy[3]}-${dmy[2]}-${dmy[1]}`; else if (/^\d{4}-\d{2}-\d{2}$/.test(v)) iso = v; else return ''; const [y, mo, da] = iso.split('-').map(Number); const dt = new Date(Date.UTC(y, mo - 1, da)); return (dt.getUTCFullYear() === y && dt.getUTCMonth() === mo - 1 && dt.getUTCDate() === da) ? iso : ''; } const ACCESS_DURATIONS = [ { key: '24h', label: 'Valid for 24 hours' }, { key: '15d', label: 'Valid for 15 days' }, { key: '30d', label: 'Valid for 30 days' }, { key: 'forever', label: 'Valid forever' }, ]; const ICON = { storage: '', caret: '', calendar: '', legislation: '', version: '', lock: '', view: '', manage: '', logs: '', account: '', folder: '', file: '', shield: '', check: '', mail: '', download: '', plus: '', trash: '', edit: '', move: '', upload: '', }; async function api(url, opts = {}) { const o = Object.assign({ headers: {} }, opts); if (o.body && !(o.body instanceof FormData)) { o.headers['Content-Type'] = 'application/json'; o.body = JSON.stringify(o.body); } const res = await fetch(url, o); if (res.status === 401) { location.href = '/login.html'; throw new Error('Not authenticated'); } const ct = res.headers.get('content-type') || ''; const data = ct.includes('application/json') ? await res.json() : await res.text(); if (!res.ok) throw new Error((data && data.error) || 'Request failed'); return data; } // Upload with progress. fetch() cannot report upload progress, so this uses // XMLHttpRequest and calls onProgress(percentOrNull) as bytes go out. function apiUpload(url, formData, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', url); xhr.upload.addEventListener('progress', (e) => { if (onProgress) onProgress(e.lengthComputable ? Math.round((e.loaded / e.total) * 100) : null); }); xhr.addEventListener('load', () => { if (xhr.status === 401) { location.href = '/login.html'; return reject(new Error('Not authenticated')); } let data = null; try { data = JSON.parse(xhr.responseText); } catch (_) { /* non-JSON */ } if (xhr.status >= 200 && xhr.status < 300) return resolve(data || {}); reject(new Error((data && data.error) || `Upload failed (${xhr.status})`)); }); xhr.addEventListener('error', () => reject(new Error('Network error during upload.'))); xhr.addEventListener('abort', () => reject(new Error('Upload cancelled.'))); xhr.send(formData); }); } function toast(msg, kind = '') { let box = document.querySelector('.toasts'); if (!box) { box = document.createElement('div'); box.className = 'toasts'; document.body.appendChild(box); } const t = document.createElement('div'); t.className = 'toast ' + kind; t.textContent = msg; box.appendChild(t); setTimeout(() => t.remove(), 3600); } function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } function fmtBytes(n) { if (!n) return '0 B'; const u = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; n = Number(n); while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } return `${n.toFixed(i ? 1 : 0)} ${u[i]}`; } function modal(title, bodyHtml, opts = {}) { const back = document.createElement('div'); back.className = 'modal-back'; back.innerHTML = `