new-site/performancewest_erpnext/performancewest_erpnext/www/cdr-upload.html
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

178 lines
7.9 KiB
HTML

{% extends "templates/web.html" %}
{% block page_content %}
<style>
.pw-cdr-wrap { max-width: 860px; margin: 0 auto; }
.pw-cdr-wrap h1 { color: #1e3a5f; margin: 0 0 1rem; }
.pw-cdr-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 1.25rem 1.5rem; margin: 1rem 0; }
.pw-drop {
border: 2px dashed #cbd5e1; border-radius: 12px; padding: 2rem 1rem;
text-align: center; color: #475569; background: #f8fafc;
}
.pw-drop.drag { border-color: #059669; background: #ecfdf5; color: #065f46; }
.pw-lock-banner {
background: #fef3c7; border-left: 4px solid #b45309;
padding: 1rem 1.25rem; border-radius: 0 8px 8px 0; margin: 1rem 0;
}
.pw-unlock-banner {
background: #ecfdf5; border-left: 4px solid #059669;
padding: 1rem 1.25rem; border-radius: 0 8px 8px 0; margin: 1rem 0;
}
.pw-stat-row { display: flex; gap: 1rem; flex-wrap: wrap; }
.pw-stat { flex: 1 1 180px; background: #f8fafc; padding: 1rem; border-radius: 8px; }
.pw-stat strong { font-size: 1.4rem; color: #1e3a5f; display: block; }
.pw-btn { padding: .6rem 1.25rem; border: 0; border-radius: 6px; background: #059669; color: #fff; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-block; }
</style>
<div class="pw-cdr-wrap">
<h1>CDR Upload &amp; Traffic Study</h1>
{% if not profile %}
<div class="pw-cdr-card">
<p>No CDR ingestion profile yet.</p>
<p><a href="/cdr-settings" class="pw-btn">Set up ingestion →</a></p>
</div>
{% else %}
<div class="pw-cdr-card">
<h2 style="color:#1e3a5f; font-size:1.2rem; margin:0 0 .75rem;">Drop a CDR file</h2>
<div id="pw_drop" class="pw-drop">
<p>Drag &amp; drop a CDR (CSV, NDJSON, CSV.gz) here, or click to choose.</p>
<input id="pw_file" type="file" style="display:none" accept=".csv,.csv.gz,.ndjson,.json,.gz,.txt">
<button class="pw-btn" onclick="document.getElementById('pw_file').click()">Choose file</button>
</div>
<p style="margin-top:1rem; font-size:.85rem; color:#64748b;">
Upload goes straight to encrypted storage scoped to your account.
Processing starts within a minute. Files &gt; 500 MB: use the
secure file drop (SFTP/FTPS) from <a href="/cdr-settings">settings</a>.
</p>
</div>
<div id="pw_study" class="pw-cdr-card">
<h2 style="color:#1e3a5f; font-size:1.2rem; margin:0 0 .75rem;">
{{ reporting_year }} traffic study
</h2>
<div id="pw_study_body">Loading…</div>
</div>
{% endif %}
</div>
<script>
(function () {
const profileId = {{ profile.id if profile else "null" }};
const year = {{ reporting_year }};
if (!profileId) return;
// ── Drag-drop upload ──────────────────────────────────────────────
const drop = document.getElementById("pw_drop");
const fileInput = document.getElementById("pw_file");
["dragenter","dragover"].forEach((e) =>
drop.addEventListener(e, (ev) => { ev.preventDefault(); drop.classList.add("drag"); }));
["dragleave","drop"].forEach((e) =>
drop.addEventListener(e, (ev) => { ev.preventDefault(); drop.classList.remove("drag"); }));
drop.addEventListener("drop", (ev) => { if (ev.dataTransfer.files[0]) uploadFile(ev.dataTransfer.files[0]); });
fileInput.addEventListener("change", () => { if (fileInput.files[0]) uploadFile(fileInput.files[0]); });
async function uploadFile(file) {
drop.innerHTML = `<p>Uploading ${file.name}…</p>`;
try {
const tokenResp = await fetch("/api/v1/cdr/upload-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({ profile_id: profileId, file_name: file.name }),
});
const token = await tokenResp.json();
// Actual PUT would use the pre-signed MinIO URL; the API layer
// constructs it from token. Here we proxy through the same API.
const putResp = await fetch(`/api/v1/cdr/upload/${token.token}`, {
method: "PUT",
body: file,
});
if (!putResp.ok) throw new Error(`HTTP ${putResp.status}`);
drop.innerHTML = `<p style="color:#065f46;">✓ ${file.name} uploaded.
Processing in queue; refresh in a minute to see the study update.</p>`;
setTimeout(loadStudy, 30000);
} catch (err) {
drop.innerHTML = `<p style="color:#b91c1c;">Upload failed: ${err.message}</p>`;
}
}
// ── Paywall-aware study render ────────────────────────────────────
async function loadStudy() {
const body = document.getElementById("pw_study_body");
try {
const resp = await fetch(`/api/v1/cdr/profile/${profileId}/study?year=${year}`);
const data = await resp.json();
const ing = data.ingestion || {};
const healthBlock = `
<div class="pw-stat-row">
<div class="pw-stat">
<strong>${(ing.rows_this_year || 0).toLocaleString()}</strong>
calls classified in ${year}
</div>
<div class="pw-stat">
<strong>${((ing.bytes_stored || 0) / 1073741824).toFixed(2)} GB</strong>
compressed CDR data on file
</div>
<div class="pw-stat">
<strong>${ing.rows_quarantined || 0}</strong>
rows in quarantine
<a href="/cdr-quarantine" style="font-size:.85rem">review →</a>
</div>
</div>
`;
if (data.status === "locked") {
body.innerHTML = healthBlock + `
<div class="pw-lock-banner">
<strong>${year} classified study is locked.</strong>
<p style="margin:.3rem 0 .75rem;">${data.unlock_reason}</p>
<a class="pw-btn" href="${data.unlock_url}">Unlock ${year} traffic study →</a>
</div>
`;
return;
}
if (data.status === "unlocked_pending_study") {
body.innerHTML = healthBlock + `
<div class="pw-unlock-banner">
<strong>Unlocked.</strong> Your ${year} study will generate on the next CDRAnalysisHandler run — usually within 24 hours of payment.
</div>
`;
return;
}
// Unlocked
const rep = data.classified_report || {};
body.innerHTML = healthBlock + `
<div class="pw-unlock-banner">
<strong>${year} study unlocked</strong> (paid via order ${data.granted_by_order}).
</div>
<table style="width:100%; border-collapse:collapse; margin-top:1rem;">
<thead style="background:#f8fafc;">
<tr><th style="text-align:left;padding:.4rem .6rem;">Metric</th>
<th style="text-align:right;padding:.4rem .6rem;">Revenue-weighted</th>
<th style="text-align:right;padding:.4rem .6rem;">Minutes-weighted</th></tr>
</thead>
<tbody>
${["interstate","intrastate","international","indeterminate"].map((j) => `
<tr><td style="padding:.4rem .6rem;">${j[0].toUpperCase() + j.slice(1)}</td>
<td style="text-align:right;padding:.4rem .6rem;">${fmt(rep[j+"_pct"])}</td>
<td style="text-align:right;padding:.4rem .6rem;">${fmt(rep[j+"_pct_minutes"])}</td></tr>
`).join("")}
</tbody>
</table>
<p style="margin-top:1rem;">
${rep.pdf_minio_path
? `<a class="pw-btn" href="/api/v1/cdr/study-download?profile=${profileId}&year=${year}&fmt=pdf">Download PDF</a>
<a class="pw-btn" style="background:#2d4e78;margin-left:.5rem;" href="/api/v1/cdr/study-download?profile=${profileId}&year=${year}&fmt=xlsx">Download XLSX</a>`
: "<em>Study PDF/XLSX being generated…</em>"}
</p>
`;
} catch (err) {
body.innerHTML = `<p style="color:#b91c1c;">Could not load study: ${err.message}</p>`;
}
}
function fmt(n) { return (n == null) ? "—" : (Number(n).toFixed(2) + "%"); }
loadStudy();
})();
</script>
{% endblock %}