Files
ISDSS---Internal-Survailanc…/public/view-data.html
T
2026-09-13 20:12:28 +01:00

214 lines
8.1 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>View Data · Martinhal ISDSS</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<script src="/js/app.js"></script>
<script>
(async () => {
const shell = await buildShell('view'); if (!shell) return;
const { isAdmin, content } = shell;
let state = { folders: [], files: [], requestState: {} };
async function load() {
state = await api('/api/data/tree');
setClockOffset(state.server_time);
render();
startTimers();
}
function children(parentId) {
return {
folders: state.folders.filter((f) => (f.parent_id || null) === parentId),
files: state.files.filter((f) => (f.folder_id || null) === parentId),
};
}
function reqBadge(type, id) {
const s = state.requestState[`${type}:${id}`];
if (s === 'approved') return `<span class="badge badge-approved">Approved</span>`;
if (s === 'pending') return `<span class="badge badge-pending">Pending</span>`;
if (s === 'denied') return `<span class="badge badge-denied">Declined</span>`;
return '';
}
// Files inside a folder the user can open need no request of their own —
// access to the folder implies access to its contents.
function fileActions(f) {
if (f.accessible) {
return `<a class="btn btn-sm btn-primary" href="/api/data/download/file/${f.id}">${ICON.download} Download</a>`;
}
const s = state.requestState[`file:${f.id}`];
if (s === 'pending') return `<button class="btn btn-sm" disabled>Requested</button>`;
return `<button class="btn btn-sm" data-req="file" data-id="${f.id}">${ICON.mail} Request access</button>`;
}
function folderActions(f) {
if (f.accessible) return isAdmin ? '' : `<span class="badge badge-approved">Access granted</span>`;
const s = state.requestState[`folder:${f.id}`];
if (s === 'pending') return `<button class="btn btn-sm" disabled>Requested</button>`;
return `<button class="btn btn-sm" data-req="folder" data-id="${f.id}">${ICON.mail} Request access</button>`;
}
// Both dates are shown for every folder, whether or not access is granted.
// "Legal Validity" is the recorded date plus 30 days, calculated server-side.
function folderDates(fo) {
const rec = fo.recorded_date
? `<span class="date-value">${esc(fmtDate(fo.recorded_date))}</span>`
: '<span class="date-value muted-value">not set</span>';
let val;
if (!fo.legal_validity) {
val = '<span class="date-value muted-value">not set</span>';
} else {
const expired = fo.legal_validity < today();
val = `<span class="date-value${expired ? ' date-expired' : ''}">${esc(fmtDate(fo.legal_validity))}${
expired ? ' (expired)' : ''}</span>`;
}
return `<div class="folder-dates">
<span class="date-item"><span class="date-label">Recorded Date</span>${rec}</span>
<span class="date-item"><span class="date-label">Legal Validity</span>${val}</span>
<span class="date-item"><span class="date-label">Access</span>${accessCell(fo)}</span>
</div>`;
}
function today() { return new Date().toISOString().slice(0, 10); }
// ---- Access countdown -------------------------------------------------
// The span carries the expiry as a data attribute; a single ticking timer
// updates every one of them once a second.
function accessCell(fo) {
const a = fo.access || { state: 'denied' };
if (a.state === 'admin') {
return '<span class="access-timer access-perm">Full access (administrator)</span>';
}
if (a.state === 'permanent') {
return '<span class="access-timer access-perm">Access granted — no expiry</span>';
}
if (a.state === 'timed') {
return `<span class="access-timer access-live" data-expires="${esc(a.expires_at)}">…</span>`;
}
return '<span class="access-timer access-denied">Access Denied</span>';
}
// Difference between server time and this browser's clock, so the countdown
// stays honest even if the local clock is wrong.
let clockOffset = 0;
function setClockOffset(serverTime) {
if (!serverTime) return;
const server = Date.parse(String(serverTime).replace(' ', 'T') + 'Z');
if (!Number.isNaN(server)) clockOffset = server - Date.now();
}
function remainingText(expiresAt) {
const end = Date.parse(String(expiresAt).replace(' ', 'T') + 'Z');
if (Number.isNaN(end)) return '—';
let ms = end - (Date.now() + clockOffset);
if (ms <= 0) return 'expired';
const s = Math.floor(ms / 1000);
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
const pad = (n) => String(n).padStart(2, '0');
if (d > 0) return `${d}d ${pad(h)}h ${pad(m)}m ${pad(sec)}s`;
return `${pad(h)}h ${pad(m)}m ${pad(sec)}s`;
}
let tickHandle = null;
function tickTimers() {
const nodes = document.querySelectorAll('.access-live[data-expires]');
let anyExpired = false;
nodes.forEach((el) => {
const txt = remainingText(el.dataset.expires);
el.textContent = txt === 'expired' ? 'Access Denied' : txt;
if (txt === 'expired') {
el.classList.remove('access-live');
el.classList.add('access-denied');
anyExpired = true;
} else {
// warn when under an hour remains
el.classList.toggle('access-soon', (Date.parse(String(el.dataset.expires).replace(' ', 'T') + 'Z')
- (Date.now() + clockOffset)) < 3600000);
}
});
// Once something lapses, refresh so the folder's contents disappear too.
if (anyExpired) load();
}
function startTimers() {
if (tickHandle) clearInterval(tickHandle);
tickTimers();
tickHandle = setInterval(tickTimers, 1000);
}
function renderNode(parentId) {
const { folders, files } = children(parentId);
if (!folders.length && !files.length) return '';
let html = '<ul class="tree">';
for (const fo of folders) {
// A locked folder shows its name only: no contents, no counts.
const locked = !fo.accessible;
html += `<li>
<div class="row${locked ? ' locked' : ''}">
<span class="ic folder">${locked ? ICON.lock : ICON.folder}</span>
<span class="name">${esc(fo.name)}</span> ${reqBadge('folder', fo.id)}
${locked ? '<span class="locked-note">Access required</span>' : ''}
<span class="spacer"></span>
<span class="actions" style="opacity:1">${folderActions(fo)}</span>
</div>
${folderDates(fo)}
${locked ? '' : renderNode(fo.id)}
</li>`;
}
for (const fi of files) {
html += `<li>
<div class="row">
<span class="ic">${ICON.file}</span>
<span class="name">${esc(fi.name)}</span>
<span class="meta">${fmtBytes(fi.size)}</span> ${fi.accessible ? '' : reqBadge('file', fi.id)}
<span class="spacer"></span>
<span class="actions" style="opacity:1">${fileActions(fi)}</span>
</div>
</li>`;
}
return html + '</ul>';
}
function render() {
const tree = renderNode(null);
content.innerHTML = `
<div class="page-head">
<h1>View Data</h1>
<p>${isAdmin
? 'Everything published in Data Management, as your users will see it.'
: 'Request access to a folder to see what it contains. Once approved, everything inside is available to you.'}</p>
</div>
<div class="card">
<div class="card-head"><h2>Available data</h2></div>
<div class="card-body">
${tree || `<div class="empty">${ICON.folder}<div>No data has been published yet.</div></div>`}
</div>
</div>`;
content.querySelectorAll('[data-req]').forEach((btn) => {
btn.onclick = async () => {
btn.disabled = true;
try {
await api('/api/data/request', { method: 'POST', body: { target_type: btn.dataset.req, target_id: Number(btn.dataset.id) } });
toast('Request sent to administrators', 'ok');
await load();
} catch (e) { toast(e.message, 'err'); btn.disabled = false; }
};
});
}
load();
})();
</script>
</body>
</html>