Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 85 additions & 10 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,38 @@ const DEFAULT_LOCALITIES = [

const WASTE_TYPES = ['Food waste (wet)', 'Vegetable scraps', 'Mixed kitchen waste', 'Biodegradable packaging'];
const SHIFTS = ['Morning Shift (08:00 - 12:00)', 'Evening Shift (16:00 - 20:00)'];

/**
* CO₂ offset emission factors (kg CO₂eq per kg bio-waste) keyed by waste type and processing method.
* Source: IPCC 2006 Guidelines for National Greenhouse Gas Inventories, Volume 5 (Waste);
* GHG Protocol Scope 3 Technical Guidance.
*/
const CO2_FACTORS = {
'Food waste (wet)': { anaerobic_digestion: 0.67, composting: 0.20, biogas: 0.58, default: 0.67 },
'Vegetable scraps': { anaerobic_digestion: 0.54, composting: 0.18, biogas: 0.45, default: 0.54 },
'Mixed kitchen waste': { anaerobic_digestion: 0.60, composting: 0.22, biogas: 0.52, default: 0.60 },
'Biodegradable packaging': { anaerobic_digestion: 0.35, composting: 0.12, biogas: 0.28, default: 0.35 }
};

const PROCESSING_METHODS = {
anaerobic_digestion: 'Anaerobic Digestion',
composting: 'Composting',
biogas: 'Biogas Recovery'
};

/**
* Resolves the correct CO₂ offset factor for a given waste type and processing method.
* Falls back to a conservative estimate if the combination is not found.
* @param {string} wasteType - The waste category (must match a key in CO2_FACTORS).
* @param {string} [processingMethod] - The plant's processing method key.
* @returns {number} CO₂ offset factor in kg CO₂eq per kg waste.
*/
function getCO2Factor(wasteType, processingMethod) {
const typeFactors = CO2_FACTORS[wasteType];
if (!typeFactors) return 0.55; // Conservative fallback for unrecognised waste types
return typeFactors[processingMethod] || typeFactors['default'] || 0.55;
}
window.getCO2Factor = getCO2Factor;
const NOTIF_STORE_KEY = 'notifications';
const OFFLINE_QUEUE_KEY = 'offline-sync-queue';
const MAX_NOTIF_HISTORY = 60;
Expand Down Expand Up @@ -3123,8 +3155,12 @@ async function renderProvider(mc, fullRender) {
}),
renderMetricCard({
title: 'CO₂ Offset (kg)',
value: orders.length ? Math.round(totalKg * 0.62) : null,
description: orders.length ? 'Estimated emissions avoided from recovered waste.' : 'No offset can be calculated until loads are processed.',
value: orders.length ? Math.round(orders.reduce((sum, o) => {
const kg = parseFloat(o.actualKg || o.kg) || 0;
const plantAcc = DB.get('acc:' + o.plantId);
return sum + (kg * getCO2Factor(o.wasteType, plantAcc?.processingMethod));
}, 0)) : null,
description: orders.length ? 'Estimated emissions avoided from recovered waste (IPCC 2006 factors).' : 'No offset can be calculated until loads are processed.',
status: offsetState === 'empty' ? 'empty' : 'active',
icon: '🌍',
statusLabel: offsetState === 'empty' ? 'No data' : 'Active',
Expand Down Expand Up @@ -3314,9 +3350,13 @@ function initPvChart() {
// Dump all into current day for simplicity in local demo without real dates over weeks
const totKg = orders.reduce((s,o)=>s+parseInt(o.actualKg||o.kg), 0);
kgData[6] = totKg;
co2Data[6] = Math.round(totKg * 0.62);

window._pvDynamicData = { kg: kgData, co2: co2Data, totKg };
co2Data[6] = Math.round(orders.reduce((sum, o) => {
const kg = parseInt(o.actualKg || o.kg) || 0;
const plantAcc = DB.get('acc:' + o.plantId);
return sum + (kg * getCO2Factor(o.wasteType, plantAcc?.processingMethod));
}, 0));

window._pvDynamicData = { kg: kgData, co2: co2Data, totKg, totCO2: co2Data[6] };

pvChartInstance = new Chart(ctx, {
type: 'bar',
Expand Down Expand Up @@ -3346,7 +3386,7 @@ window.updatePvChart = function(period) {
if(period === 'monthly') {
pvChartInstance.data.labels = ['Week 1', 'Week 2', 'Week 3', 'This Week'];
pvChartInstance.data.datasets[0].data = [0, 0, 0, d.totKg];
pvChartInstance.data.datasets[1].data = [0, 0, 0, Math.round(d.totKg*0.62)];
pvChartInstance.data.datasets[1].data = [0, 0, 0, d.totCO2];
} else {
pvChartInstance.data.labels = ['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5', 'Day 6', 'Today'];
pvChartInstance.data.datasets[0].data = d.kg;
Expand Down Expand Up @@ -4360,6 +4400,14 @@ async function renderPlant(mc, fullRender) {
<div class="form-group"><label class="form-label">Biogas Produced (m³)</label><input class="form-input" id="out-bio" type="number" step="0.1"></div>
<div class="form-group"><label class="form-label">Compost Yield (kg)</label><input class="form-input" id="out-comp" type="number" step="0.1"></div>
<div class="form-group"><label class="form-label">Digester Temp (°C) <span style="font-size:11px; color:var(--amber)">(Auto-detected)</span></label><input class="form-input" id="out-temp" type="number" step="0.1" readonly placeholder="Fetching live temp..."></div>
<div class="form-group">
<label class="form-label">Processing Method <span style="font-size:11px; color:var(--text-muted)">(Applied to CO₂ offset calculations)</span></label>
<select class="form-input" id="out-method">
<option value="anaerobic_digestion">Anaerobic Digestion</option>
<option value="composting">Composting</option>
<option value="biogas">Biogas Recovery</option>
</select>
</div>
<button class="btn btn-primary btn-full" onclick="savePlantLog()">Save Record</button>
</div>
</div>
Expand All @@ -4370,6 +4418,11 @@ async function renderPlant(mc, fullRender) {
document.getElementById('out-temp').value = Math.round(w.temperature + 15);
}
});
// Pre-select the plant's saved processing method
const methodEl = document.getElementById('out-method');
if (methodEl && SESSION.processingMethod) {
methodEl.value = SESSION.processingMethod;
}
}
}
}
Expand Down Expand Up @@ -4499,7 +4552,8 @@ await recordTrustEvent(o, 'completed', 'plant', { lat: SESSION.lat, lng: SESSION
if (route.start && route.end) {
const distanceKm = parseFloat(distanceKm(route.start.lat, route.start.lng, route.end.lat, route.end.lng).toFixed(1));
const emissionKg = parseFloat((distanceKm * 0.21).toFixed(2));
const offsetKg = parseFloat((kgProcessed * 0.62).toFixed(2));
const plantAcc = DB.get('acc:' + o.plantId);
const offsetKg = parseFloat((kgProcessed * getCO2Factor(o.wasteType, plantAcc?.processingMethod)).toFixed(2));
const score = Math.max(10, Math.min(100, Math.round((offsetKg / Math.max(emissionKg, 1)) * 100)));
addEmissionsEntry({
id: 'ems-' + uid(),
Expand Down Expand Up @@ -4533,9 +4587,29 @@ await recordTrustEvent(o, 'completed', 'plant', { lat: SESSION.lat, lng: SESSION
window.savePlantLog = function() {
const bio = document.getElementById('out-bio').value;
const comp = document.getElementById('out-comp').value;
if(!bio && !comp) return window.showToast("⚠ Enter output values.");

DB.set('log:'+uid(), { id: uid(), ts: ts(), plantId: SESSION.id, bio, comp, temp: document.getElementById('out-temp').value });

if(!bio && !comp)
return window.showToast("⚠ Enter output values.");

// Persist processing method to plant account so CO₂ factor is applied consistently
const method =
document.getElementById('out-method')?.value ||
'anaerobic_digestion';

if (SESSION.processingMethod !== method) {
SESSION.processingMethod = method;
DB.set('acc:' + SESSION.id, SESSION, { localOnly: true });
}

DB.set('log:' + uid(), {
id: uid(),
ts: ts(),
plantId: SESSION.id,
bio,
comp,
temp: document.getElementById('out-temp').value
});

addWorkflowNotification({
title: 'Plant Output Logged',
body: `Your plant output record was saved successfully.`,
Expand All @@ -4544,6 +4618,7 @@ window.savePlantLog = function() {
priority: 'normal',
url: '/'
});

window.showToast("✓ Output logged! Automated msg sent.");
showView('v-pl-dash');
}
Expand Down
44 changes: 43 additions & 1 deletion src/esg-reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,8 +353,22 @@ export const ESGReporter = {

// Calculate Metrics
const totalKg = history.reduce((sum, o) => sum + (parseFloat(o.actualKg || o.kg) || 0), 0);
const totalCO2 = Math.round(totalKg * 0.62); // 0.62 kg CO2 per kg bio-waste
const totalTokens = account.tokens || 0;

// Per-order CO₂ calculation using waste-type-specific IPCC 2006 / GHG Protocol factors
const co2Details = history.map(o => {
const kg = parseFloat(o.actualKg || o.kg) || 0;
const factor = window.getCO2Factor
? window.getCO2Factor(o.wasteType, o.processingMethod)
: 0.55;
return {
wasteType: o.wasteType || 'Mixed kitchen waste',
kg,
factor,
co2: kg * factor
};
});
const totalCO2 = Math.round(co2Details.reduce((sum, d) => sum + d.co2, 0));

const activeSegScores = history.filter(o => o.segScore != null || o.quality);
const avgSegScore = activeSegScores.length
Expand Down Expand Up @@ -490,6 +504,34 @@ export const ESGReporter = {
</tbody>
</table>

<h3 style="border-bottom:2px solid #E2E8F0; padding-bottom:8px; margin-bottom:16px; margin-top:40px;">Emission Factor Methodology</h3>
<table style="width:100%; border-collapse:collapse; font-size:12px; margin-bottom:16px;">
<thead>
<tr style="background:#F1F5F9;">
<th style="padding:8px; text-align:left; border:1px solid #E2E8F0;">Waste Type</th>
<th style="padding:8px; text-align:right; border:1px solid #E2E8F0;">Qty (kg)</th>
<th style="padding:8px; text-align:right; border:1px solid #E2E8F0;">Factor (kg CO₂eq/kg)</th>
<th style="padding:8px; text-align:right; border:1px solid #E2E8F0;">CO₂ Offset (kg)</th>
</tr>
</thead>
<tbody>
${co2Details.map(d => `
<tr>
<td style="padding:8px; border:1px solid #E2E8F0;">${d.wasteType}</td>
<td style="padding:8px; text-align:right; border:1px solid #E2E8F0;">${d.kg.toFixed(1)}</td>
<td style="padding:8px; text-align:right; border:1px solid #E2E8F0;">${d.factor.toFixed(2)}</td>
<td style="padding:8px; text-align:right; border:1px solid #E2E8F0;">${d.co2.toFixed(1)}</td>
</tr>`).join('')}
</tbody>
</table>
<p style="font-size:11px; color:#64748B; margin-bottom:32px;">
<strong>Methodology:</strong> Emission factors sourced from IPCC 2006 Guidelines for National Greenhouse Gas
Inventories (Volume 5 — Waste) and the GHG Protocol Scope 3 Technical Guidance.
Factors vary by waste type and processing method (anaerobic digestion, composting, or biogas recovery).
</p>
<div style="margin-top:40px; text-align:center; font-size:11px; color:#94A3B8;">
<p>This document is digitally generated and verifiable via the ReGenX smart ledger.</p>
<p style="font-family:monospace; background:#F1F5F9; display:inline-block; padding:4px 8px; border-radius:4px;">Signature Hash (SHA-256): ${reportHash}</p>
<!-- Cryptographic Ledger Footer -->
<div style="margin-top:60px; text-align:center; font-size:10px; color:#94A3B8; border-top:1px solid #E2E8F0; padding-top:20px;">
<p style="margin:0 0 6px 0;">This ESG report was digitally compiled and is cryptographically verifiable via ReGenX zero-trust ledger API.</p>
Expand Down