v3: PostgreSQL backend — 20K rows seeded, server-side SQL filtering
- Added PostgreSQL 16 Alpine service to Docker Swarm stack - db.py: schema for 17 tables, auto-seed from jv_data.json on first boot - /api/dashboard/data?start=&end= — server-side SQL filtering All Time: 0.43s (was 4MB JSON download stuck loading) Filtered (12mo): 0.20s with ~90% less data transferred - Dashboard HTML patched: calls API instead of static JSON - Integer casting for IsNewCustomer/HasDiscount/IsFreeShipping - Advisory lock prevents race condition during parallel worker startup - Returning Revenue now shows correctly: £14.5M (75% of total)
This commit is contained in:
@@ -114,6 +114,7 @@ tr:hover td{background:rgba(99,102,241,.04)}
|
||||
<button class="preset-btn" onclick="setPreset('all')" id="presetAll">All Time</button>
|
||||
</div>
|
||||
<div class="filter-info" id="filterInfo"></div>
|
||||
<div id="dbBadge" style="margin-left:8px"></div>
|
||||
</div>
|
||||
|
||||
<div id="loadingMsg" class="loading"><span class="spinner"></span>Loading data…</div>
|
||||
@@ -277,7 +278,8 @@ function applyFilter(){
|
||||
FSTART = s || null;
|
||||
FEND = e || null;
|
||||
document.querySelectorAll('.preset-btn').forEach(b=>b.classList.remove('active'));
|
||||
buildAll();
|
||||
// Re-fetch from PostgreSQL with server-side filtering
|
||||
loadData(FSTART, FEND);
|
||||
}
|
||||
|
||||
function resetFilter(){
|
||||
@@ -286,7 +288,8 @@ function resetFilter(){
|
||||
document.getElementById('endDate').value = '';
|
||||
document.querySelectorAll('.preset-btn').forEach(b=>b.classList.remove('active'));
|
||||
document.getElementById('presetAll').classList.add('active');
|
||||
buildAll();
|
||||
// Re-fetch full dataset from PostgreSQL
|
||||
loadData();
|
||||
}
|
||||
|
||||
function setPreset(key){
|
||||
@@ -314,7 +317,8 @@ function setPreset(key){
|
||||
}
|
||||
document.getElementById('startDate').value = FSTART || '';
|
||||
document.getElementById('endDate').value = FEND || '';
|
||||
buildAll();
|
||||
// Re-fetch from PostgreSQL with server-side filtering
|
||||
loadData(FSTART, FEND);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -869,25 +873,41 @@ function buildCustomers(fm){
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// LOAD DATA
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
async function loadData(){
|
||||
async function loadData(start, end){
|
||||
try {
|
||||
const res = await fetch('jv_data.json');
|
||||
// Build API URL with optional date filters — data served from PostgreSQL
|
||||
let url = '/api/dashboard/data';
|
||||
const params = [];
|
||||
if(start) params.push('start=' + encodeURIComponent(start));
|
||||
if(end) params.push('end=' + encodeURIComponent(end));
|
||||
if(params.length) url += '?' + params.join('&');
|
||||
|
||||
const t0 = performance.now();
|
||||
const res = await fetch(url);
|
||||
if(!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
RAW = await res.json();
|
||||
const elapsed = ((performance.now()-t0)/1000).toFixed(2);
|
||||
|
||||
// Sort monthly data
|
||||
RAW.monthly.sort((a,b) => a.YearMonth.localeCompare(b.YearMonth));
|
||||
|
||||
// Set date picker min/max
|
||||
const allMonths = RAW.monthly.map(r=>r.YearMonth);
|
||||
document.getElementById('startDate').min = allMonths[0];
|
||||
document.getElementById('startDate').max = allMonths[allMonths.length-1];
|
||||
document.getElementById('endDate').min = allMonths[0];
|
||||
document.getElementById('endDate').max = allMonths[allMonths.length-1];
|
||||
// Set date picker min/max from meta (full range)
|
||||
if(RAW.meta && RAW.meta.dateMin){
|
||||
document.getElementById('startDate').min = RAW.meta.dateMin;
|
||||
document.getElementById('startDate').max = RAW.meta.dateMax;
|
||||
document.getElementById('endDate').min = RAW.meta.dateMin;
|
||||
document.getElementById('endDate').max = RAW.meta.dateMax;
|
||||
}
|
||||
|
||||
document.getElementById('loadingMsg').style.display = 'none';
|
||||
document.getElementById('mainContent').style.display = 'block';
|
||||
document.getElementById('presetAll').classList.add('active');
|
||||
if(!start && !end) document.getElementById('presetAll').classList.add('active');
|
||||
|
||||
// Show data source badge
|
||||
const src = RAW._source || 'json';
|
||||
const qt = RAW._query_time || elapsed+'s';
|
||||
const badge = document.getElementById('dbBadge');
|
||||
if(badge) badge.innerHTML = `<span style="font-size:10px;color:var(--green);background:rgba(16,185,129,.1);padding:2px 8px;border-radius:4px;font-family:monospace">⚡ PostgreSQL · ${qt} · ${RAW.monthly.length} months</span>`;
|
||||
|
||||
buildAll();
|
||||
} catch(err) {
|
||||
@@ -895,9 +915,8 @@ async function loadData(){
|
||||
<div style="color:var(--red);font-size:16px;margin-bottom:12px">⚠ Failed to load data</div>
|
||||
<div style="color:var(--muted);font-size:13px">${err.message}</div>
|
||||
<div style="color:var(--muted);font-size:12px;margin-top:12px">
|
||||
Make sure <code>jv_data.json</code> is in the same folder as this HTML file.<br>
|
||||
Run <code>python preprocess_jv_data.py</code> first to generate it.<br>
|
||||
Then serve via a local server: <code>python -m http.server 8000</code>
|
||||
Data is served from PostgreSQL via <code>/api/dashboard/data</code>.<br>
|
||||
Check that the database service is running.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user