new-site/performancewest_erpnext/performancewest_erpnext/www/cdr-settings.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

246 lines
11 KiB
HTML

{% extends "templates/web.html" %}
{% block page_content %}
<style>
:root { --pw-navy:#1e3a5f; --pw-blue:#2d4e78; --pw-light:#f8fafc; --pw-green:#059669; --pw-amber:#b45309; }
.pw-cdr-wrap { max-width: 900px; margin: 0 auto; }
.pw-cdr-wrap h1 { color: var(--pw-navy); 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-cdr-card h2 { color: var(--pw-navy); font-size: 1.2rem; margin: 0 0 .75rem; }
.pw-field { display: block; margin: 1rem 0 .25rem; font-weight: 600; color: #1f2937; }
.pw-input { width: 100%; padding: .55rem .7rem; border: 1px solid #cbd5e1; border-radius: 6px; font-size: .95rem; }
.pw-row { display: flex; gap: 1rem; flex-wrap: wrap; }
.pw-row > * { flex: 1 1 250px; }
.pw-btn { padding: .6rem 1.25rem; border: 0; border-radius: 6px; background: var(--pw-green); color: #fff; font-weight: 600; cursor: pointer; }
.pw-btn-secondary { background: var(--pw-blue); }
.pw-btn-plain { background: #e2e8f0; color: #1f2937; }
.pw-callout { border-left: 4px solid var(--pw-blue); background: #f0f9ff; padding: .9rem 1.1rem; border-radius: 0 6px 6px 0; margin: 1rem 0; }
.pw-help { color: #64748b; font-size: .85rem; margin-top: .2rem; }
.pw-badge-ok { background: #d1fae5; color: #065f46; padding: .1rem .5rem; border-radius: 10px; font-size: .8rem; font-weight: 600; }
.pw-badge-off { background: #e2e8f0; color: #475569; padding: .1rem .5rem; border-radius: 10px; font-size: .8rem; font-weight: 600; }
.pw-badge-warn { background: #fef3c7; color: #92400e; padding: .1rem .5rem; border-radius: 10px; font-size: .8rem; font-weight: 600; }
.pw-quota-bar { height: 10px; background: #e2e8f0; border-radius: 5px; overflow: hidden; margin: .25rem 0; }
.pw-quota-fill { height: 100%; background: var(--pw-blue); transition: width .3s; }
.pw-quota-fill.warn { background: var(--pw-amber); }
.pw-quota-fill.over { background: #b91c1c; }
</style>
<div class="pw-cdr-wrap">
<h1>CDR Ingestion Settings</h1>
{% if not profile %}
<div class="pw-callout">
<strong>No CDR profile yet.</strong> Set up your carrier&apos;s CDR
ingestion and we&apos;ll start classifying calls for your 499-A traffic
study. You control whether you push CDRs to us, let us pull them from
your switch, or both.
</div>
{% endif %}
<div class="pw-cdr-card">
<h2>1. Your switch</h2>
<label class="pw-field" for="switch_preset">What switch do you use?</label>
<select id="switch_preset" class="pw-input" name="switch_preset">
{% for p in switch_presets %}
<option value="{{ p.slug }}"
{% if profile and profile.switch_preset == p.slug %}selected{% endif %}>
{{ p.label }}
</option>
{% endfor %}
</select>
<p class="pw-help">
Picking a known switch auto-selects the CDR format and shows you the
credential fields we need. Click <em>Test connection</em> before saving
to make sure the credentials work.
</p>
<div id="preset_credentials">
<!-- populated dynamically from /api/v1/cdr/preset-fields/:slug -->
</div>
<div style="margin-top: 1rem;">
<button class="pw-btn-plain pw-btn" id="test_connection">Test connection</button>
<span id="test_result" style="margin-left: .75rem; font-size: .9rem;"></span>
</div>
</div>
<div class="pw-cdr-card">
<h2>2. Secure file drop (SFTP / FTPS)</h2>
<p>
Enable a secure SFTP / FTPS endpoint scoped to your account. We generate a
random password when you turn it on &mdash; <strong>copied once</strong>.
Use this alongside switch pull, or as a standalone option.
</p>
<p>
Status:
{% if profile and profile.sftpgo_enabled %}
<span class="pw-badge-ok">Enabled</span>
<code>{{ profile.sftpgo_username }}</code>
{% else %}
<span class="pw-badge-off">Disabled</span>
{% endif %}
</p>
<button id="sftp_toggle" class="pw-btn"
data-enabled="{{ '1' if (profile and profile.sftpgo_enabled) else '0' }}">
{% if profile and profile.sftpgo_enabled %}Rotate password{% else %}Enable secure file drop{% endif %}
</button>
</div>
<div class="pw-cdr-card">
<h2>3. Revenue attribution</h2>
<label>
<input type="checkbox" id="minutes_only"
{% if profile and profile.minutes_only_estimation_enabled %}checked{% endif %}>
Use minutes-only estimation
</label>
<p class="pw-help">
Leave this <strong>off</strong> if your CDRs carry per-call revenue
(recommended &mdash; most commercial switches do). Turn it on if you
run a flat-rate line service or your switch doesn&apos;t emit per-call
charges. Your traffic study will be labeled accordingly.
</p>
<label class="pw-field" for="flat_monthly_revenue">Monthly flat revenue (USD)</label>
<input id="flat_monthly_revenue" type="number" step="1" class="pw-input"
value="{{ (profile.flat_monthly_revenue_cents / 100) if (profile and profile.flat_monthly_revenue_cents) else '' }}"
placeholder="only needed when minutes-only is on">
</div>
<div class="pw-cdr-card">
<h2>4. Storage &amp; retention</h2>
{% if profile %}
<p>Current plan: <strong>{{ profile.storage_plan|title }}</strong></p>
<!-- Usage meter populated asynchronously -->
<div id="quota_meter">Loading usage…</div>
<p class="pw-help">At 80% we email a heads-up. At 100%, behavior follows the over-quota policy below.</p>
<label class="pw-field" for="over_quota_policy">Over-quota policy</label>
<select id="over_quota_policy" class="pw-input">
<option value="notify" {% if profile.over_quota_policy == "notify" %}selected{% endif %}>Pause classification, notify admin</option>
<option value="block" {% if profile.over_quota_policy == "block" %}selected{% endif %}>Reject new uploads until upgraded</option>
<option value="auto_upgrade" {% if profile.over_quota_policy == "auto_upgrade" %}selected{% endif %}>Auto-upgrade to next tier (charges saved card)</option>
</select>
{% else %}
<p>Storage quotas activate once your profile is saved. You&apos;re on the <em>Included with filing</em> plan by default.</p>
{% endif %}
<table style="width: 100%; margin-top: 1rem; font-size: .9rem; border-collapse: collapse;">
<thead style="background: var(--pw-light);">
<tr>
<th style="text-align:left; padding:.4rem .6rem;">Plan</th>
<th style="text-align:left; padding:.4rem .6rem;">Storage</th>
<th style="text-align:left; padding:.4rem .6rem;">Calls / year</th>
<th style="text-align:left; padding:.4rem .6rem;">Annual</th>
</tr>
</thead>
<tbody>
{% for t in storage_tiers %}
<tr>
<td style="padding:.4rem .6rem; border-top:1px solid #e2e8f0;">{{ t.name }}</td>
<td style="padding:.4rem .6rem; border-top:1px solid #e2e8f0;">{{ t.storage }}</td>
<td style="padding:.4rem .6rem; border-top:1px solid #e2e8f0;">{{ t.calls }}</td>
<td style="padding:.4rem .6rem; border-top:1px solid #e2e8f0;">{{ t.price }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="margin: 2rem 0; text-align: right;">
<button id="save_settings" class="pw-btn">Save settings</button>
</div>
</div>
<script>
(function () {
const profileId = {{ profile.id if profile else 'null' }};
async function api(method, path, body) {
const opts = { method, headers: { "Content-Type": "application/json" }, credentials: "same-origin" };
if (body) opts.body = JSON.stringify(body);
const resp = await fetch(path, opts);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
// Load quota meter once the page is ready
async function loadUsage() {
if (!profileId) return;
try {
const year = new Date().getUTCFullYear();
const resp = await fetch(`/api/v1/cdr/profile/${profileId}/study?year=${year}`);
const data = await resp.json();
const ing = data.ingestion || {};
const el = document.getElementById("quota_meter");
if (!el) return;
// Without per-plan caps surfaced, show raw counts.
el.innerHTML = `
<div><strong>${(ing.rows_this_year || 0).toLocaleString()}</strong> calls classified in ${year}</div>
<div><strong>${((ing.bytes_stored || 0) / 1073741824).toFixed(2)} GB</strong> of compressed CDR data on file</div>
<div>Last ingest: ${ing.last_upload_at || "—"}</div>
`;
} catch (err) { /* silent */ }
}
loadUsage();
// Test connection button: fires against a hypothetical preset-validate
// endpoint; wired to the cdr.ts route once implemented.
document.getElementById("test_connection").addEventListener("click", async () => {
const msg = document.getElementById("test_result");
msg.textContent = "Testing…";
msg.style.color = "#64748b";
try {
const preset = document.getElementById("switch_preset").value;
const resp = await api("POST", `/api/v1/cdr/profile/${profileId || 0}/test-connection`, {
switch_preset: preset,
});
if (resp.ok) {
msg.textContent = "✓ " + (resp.detail || "connected");
msg.style.color = "#059669";
} else {
msg.textContent = "✗ " + (resp.detail || "failed");
msg.style.color = "#b91c1c";
}
} catch (err) {
msg.textContent = "✗ " + err.message;
msg.style.color = "#b91c1c";
}
});
// SFTP toggle
document.getElementById("sftp_toggle").addEventListener("click", async (e) => {
const enabled = e.target.dataset.enabled === "1";
try {
const resp = await api("POST", `/api/v1/cdr/profile/${profileId}/sftpgo`,
{ action: enabled ? "rotate" : "enable" });
if (resp.password) {
alert(
"SFTP credentials — copy now, we won't show this again:\n\n" +
`Host: cdr.performancewest.net\nUser: ${resp.username}\nPass: ${resp.password}\n\n` +
"Clients: SFTP port 2022, FTPS port 990 (implicit TLS).",
);
window.location.reload();
}
} catch (err) {
alert("Could not toggle SFTP: " + err.message);
}
});
// Save settings
document.getElementById("save_settings").addEventListener("click", async () => {
const body = {
switch_preset: document.getElementById("switch_preset").value,
minutes_only_estimation_enabled: document.getElementById("minutes_only").checked,
flat_monthly_revenue_cents: Math.round(
(parseFloat(document.getElementById("flat_monthly_revenue").value) || 0) * 100
),
over_quota_policy: document.getElementById("over_quota_policy")?.value,
};
try {
await api("PUT", `/api/v1/cdr/profile/${profileId}`, body);
alert("Saved.");
} catch (err) {
alert("Save failed: " + err.message);
}
});
})();
</script>
{% endblock %}