dca / index.html
alterzick's picture
Add 2 files
dbf70f8 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Decline Curve Analysis - Oil & Gas Well Production</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
.chart-container {
position: relative;
margin: auto;
width: 100%;
max-width: 900px;
height: 500px;
}
.upload-area {
border: 2px dashed #cbd5e1;
border-radius: 12px;
padding: 3rem 2rem;
text-align: center;
background-color: #f8fafc;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-area:hover {
border-color: #4c51bf;
background-color: #ebf4ff;
}
.upload-area.highlight {
border-color: #2b6cb0;
background-color: #e6f0ff;
}
.info-card {
transition: all 0.3s ease;
border-left: 4px solid #4c51bf;
}
.info-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.manual-input {
transition: all 0.3s ease;
border-left: 3px solid #2b6cb0;
}
.manual-input:hover {
transform: translateY(-1px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
}
.filter-badge {
background: linear-gradient(90deg, #3b82f6, #60a5fa);
padding: 0.25rem 0.75rem;
border-radius: 9999px;
color: white;
font-size: 0.75rem;
font-weight: 600;
margin: 0.25rem;
display: inline-flex;
align-items: center;
}
.filter-badge i {
margin-right: 0.25rem;
font-size: 0.625rem;
}
.table-container {
max-height: 400px;
overflow-y: auto;
}
/* Custom scrollbar */
.table-container::-webkit-scrollbar {
width: 8px;
}
.table-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.table-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 10px;
}
.table-container::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
/* Add button animation */
.add-btn {
transition: all 0.2s;
}
.add-btn:hover {
transform: scale(1.05);
background-color: #1e40af !important;
}
/* Delete button styling */
.delete-btn {
background: #fee2e2;
padding: 0.25rem;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.delete-btn:hover {
background: #fecaca;
transform: scale(1.1);
}
.delete-btn i {
color: #dc2626;
font-size: 0.875rem;
}
/* Flash animation */
@keyframes flash {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.flash {
animation: flash 0.5s;
}
</style>
</head>
<body class="bg-gradient-to-br from-slate-50 to-slate-100 min-h-screen">
<div class="container mx-auto px-4 py-10">
<div class="text-center mb-10">
<h1 class="text-4xl font-bold text-gray-800 mb-3">Decline Curve Analysis</h1>
<p class="text-lg text-gray-600 max-w-3xl mx-auto">
Enter current well information, manually input production data or upload your production data in Excel format to analyze production decline and forecast future performance.
Supports exponential, hyperbolic, and harmonic decline models.
</p>
</div>
<!-- Manual Input Section for Current Month and Production -->
<div class="bg-white rounded-2xl shadow-xl p-8 mb-8 max-w-4xl mx-auto transform hover:shadow-2xl transition-shadow duration-300">
<div class="flex items-center mb-6 text-blue-700">
<i class="fas fa-pen-alt text-2xl mr-3"></i>
<h2 class="text-2xl font-semibold">Current Well Information</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8 manual-input bg-sky-50 p-6 rounded-xl border border-sky-200">
<div class="mb-4">
<label for="currentMonthInput" class="block text-sm font-medium text-gray-700 mb-2">Current Month of Production</label>
<div class="relative">
<select id="currentMonthInput" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 appearance-none bg-white">
<option value="">-- Select Month --</option>
<option value="0">Current (0 months)</option>
<option value="1">1 Month Ago</option>
<option value="2">2 Months Ago</option>
<option value="3">3 Months Ago</option>
<option value="6">6 Months Ago</option>
<option value="9">9 Months Ago</option>
<option value="12">12 Months Ago</option>
<option value="18">18 Months Ago</option>
<option value="24">2 Years Ago</option>
<option value="36">3 Years Ago</option>
<option value="48">4 Years Ago</option>
<option value="60">5 Years Ago</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-700">
<i class="fas fa-chevron-down"></i>
</div>
</div>
<p class="mt-2 text-sm text-gray-500">How many months ago is this current data point?</p>
</div>
<div class="mb-4">
<label for="currentProductionInput" class="block text-sm font-medium text-gray-700 mb-2">Current Production Rate (BOPD)</label>
<div class="relative">
<input type="number" id="currentProductionInput" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Enter current production rate" min="0" step="0.1">
<div class="absolute inset-y-0 right-0 flex items-center px-3 bg-blue-50 rounded-r-lg">
<span class="text-sm text-blue-700 font-medium">BOPD</span>
</div>
</div>
<p class="mt-2 text-sm text-gray-500">Current oil production rate in barrels of oil per day</p>
</div>
</div>
<div class="bg-amber-50 p-4 rounded-lg border border-amber-200 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<i class="fas fa-info-circle text-amber-500 text-xl"></i>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-amber-800">Important Note</h3>
<p class="mt-1 text-sm text-amber-700">
These values will be used as reference points for your analysis. They will be included in the results section before you upload your Excel file with historical data.
</p>
</div>
</div>
</div>
</div>
<!-- Manual Production Data Input Section -->
<div id="manualInputSection" class="bg-white rounded-2xl shadow-xl p-8 mb-8 max-w-6xl mx-auto transform hover:shadow-2xl transition-shadow duration-300">
<div class="flex items-center mb-6 text-green-700">
<i class="fas fa-keyboard text-2xl mr-3"></i>
<h2 class="text-2xl font-semibold">Manual Production Data Input</h2>
</div>
<div class="bg-green-50 p-6 rounded-xl border border-green-200 mb-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label for="productionDateInput" class="block text-sm font-medium text-gray-700 mb-2">Date</label>
<input type="date" id="productionDateInput" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500">
</div>
<div>
<label for="productionRateInput" class="block text-sm font-medium text-gray-700 mb-2">Production Rate (BOPD)</label>
<div class="flex">
<input type="number" id="productionRateInput" class="w-full px-4 py-3 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-green-500" placeholder="0.0" min="0" step="0.1">
<div class="bg-green-50 px-4 rounded-r-lg flex items-center">
<span class="text-sm text-green-700 font-medium">BOPD</span>
</div>
</div>
</div>
<div>
<label for="cumulativeInput" class="block text-sm font-medium text-gray-700 mb-2">Cumulative (STB)</label>
<div class="flex">
<input type="number" id="cumulativeInput" class="w-full px-4 py-3 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-green-500" placeholder="0.0" min="0" step="1">
<div class="bg-green-50 px-4 rounded-r-lg flex items-center">
<span class="text-sm text-green-700 font-medium">STB</span>
</div>
</div>
</div>
</div>
<div class="flex space-x-3">
<button id="addDataBtn" class="add-btn px-6 py-3 bg-green-600 text-white font-medium rounded-lg shadow-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50 transition-colors duration-200 flex items-center justify-center flex-1">
<i class="fas fa-plus mr-2"></i> Add Data Point
</button>
<button id="clearAllBtn" class="px-6 py-3 bg-red-600 text-white font-medium rounded-lg shadow-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 transition-colors duration-200 flex items-center">
<i class="fas fa-trash mr-2"></i> Clear All
</button>
</div>
</div>
<div class="bg-gray-50 p-6 rounded-xl border border-gray-200">
<h3 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-database mr-2"></i> Inputted Production Data
<span id="dataCount" class="ml-2 bg-blue-100 text-blue-800 text-xs font-semibold px-2.5 py-0.5 rounded-full">0 entries</span>
</h3>
<div class="table-container rounded-lg border border-gray-300 bg-white overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rate (BOPD)</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cumulative (STB)</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time (months)</th>
</tr>
</thead>
<tbody id="manualDataTableBody" class="bg-white divide-y divide-gray-200">
<!-- Manual data entries will be added here -->
</tbody>
</table>
</div>
<div class="mt-4 text-sm text-gray-500">
<p>Enter your production data manually point by point. Data will be automatically sorted by date.</p>
</div>
</div>
</div>
<!-- Upload Section with Advanced Controls -->
<div class="bg-white rounded-2xl shadow-xl p-8 mb-8 max-w-4xl mx-auto transform hover:shadow-2xl transition-shadow duration-300">
<div class="flex items-center mb-6 text-blue-700">
<i class="fas fa-chart-line text-2xl mr-3"></i>
<h2 class="text-2xl font-semibold">Upload & Configure Analysis</h2>
</div>
<!-- Analysis Configuration Section -->
<div class="bg-indigo-50 p-6 rounded-xl border border-indigo-200 mb-8">
<h3 class="text-lg font-semibold text-indigo-800 mb-4 flex items-center">
<i class="fas fa-cogs mr-2"></i> Analysis Configuration
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="declineModelSelect" class="block text-sm font-medium text-gray-700 mb-2">Decline Curve Model</label>
<div class="relative">
<select id="declineModelSelect" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 appearance-none bg-white">
<option value="hyperbolic" selected>Hyperbolic Decline</option>
<option value="exponential">Exponential Decline</option>
<option value="harmonic">Harmonic Decline</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-700">
<i class="fas fa-chevron-down"></i>
</div>
</div>
<p id="modelDescription" class="mt-2 text-sm text-gray-600">
Hyperbolic decline provides the most flexible modeling of production decline patterns.
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Date Range for Analysis</label>
<div class="grid grid-cols-2 gap-2">
<div>
<label for="startDateFilter" class="block text-xs text-gray-500 mb-1">From</label>
<input type="date" id="startDateFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500">
</div>
<div>
<label for="endDateFilter" class="block text-xs text-gray-500 mb-1">To</label>
<input type="date" id="endDateFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500">
</div>
</div>
<div id="dateFilterStatus" class="mt-2 text-sm text-gray-500">
Use date filters to analyze specific production periods.
</div>
</div>
</div>
</div>
<!-- Upload Section -->
<div id="dropZone" class="upload-area mb-6">
<i class="fas fa-cloud-upload-alt text-5xl text-gray-400 mb-4"></i>
<h3 class="text-xl font-medium text-gray-700 mb-2">Or Drag & Drop Excel File</h3>
<p class="text-gray-500 mb-4">Upload your production data file</p>
<p class="text-sm text-gray-400">Supported: .xlsx, .xls | Example columns: Date, Rate (BOPD), Cumulative (STB)</p>
<input type="file" id="fileInput" accept=".xlsx, .xls" class="hidden"/>
</div>
<div class="flex space-x-4 justify-center">
<button id="processManualDataBtn" class="px-6 py-3 bg-green-600 text-white font-medium rounded-lg shadow-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50 transition duration-200">
<i class="fas fa-chart-line mr-2"></i>Process Manual Data
</button>
<button id="uploadBtn" class="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition duration-200 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
<i class="fas fa-upload mr-2"></i>Process Excel File
</button>
</div>
</div>
<!-- Results Section -->
<div id="resultsSection" class="hidden bg-white rounded-2xl shadow-xl p-8 max-w-6xl mx-auto">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center text-green-700">
<i class="fas fa-check-circle text-2xl mr-3"></i>
<h2 class="text-2xl font-semibold">Analysis Results</h2>
</div>
<div id="activeFilters" class="flex flex-wrap"></div>
</div>
<!-- Key Metrics Row -->
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-8">
<div class="bg-blue-50 p-4 rounded-xl border border-blue-100 info-card">
<p class="text-xs text-blue-600 font-medium">Initial Rate (qi)</p>
<p id="qiValue" class="text-2xl font-bold text-blue-800">-</p>
</div>
<div class="bg-green-50 p-4 rounded-xl border border-green-100 info-card">
<p class="text-xs text-green-600 font-medium">Decline Rate (Di)</p>
<p id="diValue" class="text-2xl font-bold text-green-800">-</p>
</div>
<div class="bg-purple-50 p-4 rounded-xl border border-purple-100 info-card">
<p class="text-xs text-purple-600 font-medium">Decline Exponent (b)</p>
<p id="bValue" class="text-2xl font-bold text-purple-800">-</p>
</div>
<div class="bg-orange-50 p-4 rounded-xl border border-orange-100 info-card">
<p class="text-xs text-orange-600 font-medium">R² Fit</p>
<p id="r2Value" class="text-2xl font-bold text-orange-800">-</p>
</div>
<div class="bg-teal-50 p-4 rounded-xl border border-teal-100 info-card">
<p class="text-xs text-teal-600 font-medium">Production Life</p>
<p id="productionLife" class="text-2xl font-bold text-teal-800">-</p>
</div>
</div>
<!-- Current Performance Section -->
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 p-6 rounded-xl border border-blue-100 mb-8">
<h3 class="text-lg font-semibold text-blue-800 mb-4 flex items-center">
<i class="fas fa-fire mr-2"></i> Current Performance
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="text-center">
<p class="text-sm text-gray-600 mb-1">Current Month</p>
<p id="currentMonth" class="text-2xl font-bold text-gray-800">-</p>
</div>
<div class="text-center">
<p class="text-sm text-gray-600 mb-1">Current Production</p>
<p id="currentProduction" class="text-2xl font-bold text-green-600">-</p>
</div>
<div class="text-center">
<p class="text-sm text-gray-600 mb-1">Cumulative Production</p>
<p id="cumulativeProduction" class="text-2xl font-bold text-purple-600">-</p>
</div>
</div>
</div>
<!-- Chart -->
<div class="mb-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold text-gray-800">Production Rate vs Time</h3>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-600">Forecast:</span>
<input type="number" id="forecastMonths" class="w-20 px-3 py-2 border border-gray-300 rounded-lg text-sm text-center" value="24" min="1" max="120">
<span class="self-center text-sm text-gray-600">months</span>
</div>
</div>
<div class="chart-container">
<canvas id="productionChart"></canvas>
</div>
</div>
<!-- Data Table -->
<div class="overflow-x-auto mt-8">
<table id="dataTable" class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rate (BOPD)</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cumulative (STB)</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time (months)</th>
</tr>
</thead>
<tbody id="tableBody" class="bg-white divide-y divide-gray-200">
<!-- Data will be inserted here -->
</tbody>
</table>
</div>
<!-- Export button -->
<div class="text-right mt-6">
<button id="exportBtn" class="px-4 py-2 bg-gray-600 text-white text-sm font-medium rounded-lg hover:bg-gray-700 transition duration-200">
<i class="fas fa-download mr-2"></i>Export Results
</button>
</div>
</div>
</div>
<script>
// Global variables
let productionData = [];
let manualData = [];
let filteredData = [];
let productionChart = null;
let currentAnalysis = null;
let dataSource = null; // 'manual', 'upload', or null
// DOM Elements
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn');
const resultsSection = document.getElementById('resultsSection');
const declineModelSelect = document.getElementById('declineModelSelect');
const forecastMonths = document.getElementById('forecastMonths');
const exportBtn = document.getElementById('exportBtn');
const modelDescription = document.getElementById('modelDescription');
const startDateFilter = document.getElementById('startDateFilter');
const endDateFilter = document.getElementById('endDateFilter');
const dateFilterStatus = document.getElementById('dateFilterStatus');
const activeFilters = document.getElementById('activeFilters');
const dataCount = document.getElementById('dataCount');
// Manual data input elements
const productionDateInput = document.getElementById('productionDateInput');
const productionRateInput = document.getElementById('productionRateInput');
const cumulativeInput = document.getElementById('cumulativeInput');
const addDataBtn = document.getElementById('addDataBtn');
const clearAllBtn = document.getElementById('clearAllBtn');
const manualDataTableBody = document.getElementById('manualDataTableBody');
const processManualDataBtn = document.getElementById('processManualDataBtn');
// Current well information input elements
const currentMonthInput = document.getElementById('currentMonthInput');
const currentProductionInput = document.getElementById('currentProductionInput');
// Current performance elements
const currentMonthEl = document.getElementById('currentMonth');
const currentProductionEl = document.getElementById('currentProduction');
const cumulativeProductionEl = document.getElementById('cumulativeProduction');
const productionLifeEl = document.getElementById('productionLife');
// Model descriptions
const modelDescriptions = {
'exponential': 'Exponential decline assumes a constant percentage decline rate over time. Suitable for pressure-dominated reservoirs.',
'hyperbolic': 'Hyperbolic decline provides the most flexibility, combining elements of both exponential and harmonic decline. Often the best fit for actual production data.',
'harmonic': 'Harmonic decline assumes the decline rate decreases proportionally with production rate. Suitable for boundary-dominated flow.'
};
// Event Listeners
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', handleDragOver);
dropZone.addEventListener('dragleave', handleDragLeave);
dropZone.addEventListener('drop', handleDrop);
fileInput.addEventListener('change', handleFileSelect);
uploadBtn.addEventListener('click', processFile);
forecastMonths.addEventListener('change', updateChart);
exportBtn.addEventListener('click', exportResults);
// Model selection
declineModelSelect.addEventListener('change', function() {
modelDescription.textContent = modelDescriptions[this.value];
if (currentAnalysis) {
updateAnalysisParameters();
updateChart();
}
});
// Date filters
startDateFilter.addEventListener('change', function() {
applyDateFilter();
updateDateFilterStatus();
});
endDateFilter.addEventListener('change', function() {
applyDateFilter();
updateDateFilterStatus();
});
// Update the current performance display when manual inputs change
currentMonthInput.addEventListener('change', updateManualCurrentPerformance);
currentProductionInput.addEventListener('input', updateManualCurrentPerformance);
// Manual data input
addDataBtn.addEventListener('click', addManualDataPoint);
clearAllBtn.addEventListener('click', clearAllManualData);
processManualDataBtn.addEventListener('click', processManualData);
// Initialize with current date as default
document.addEventListener('DOMContentLoaded', function() {
const now = new Date();
const dateString = now.toLocaleDateString('default', { month: 'short', year: 'numeric' });
currentMonthEl.textContent = dateString;
// Set default model description
modelDescription.textContent = modelDescriptions[declineModelSelect.value];
// Set today as default for date input
const today = new Date().toISOString().split('T')[0];
productionDateInput.value = today;
// Set initial data count
updateDataCount();
});
// Update current performance display with manual inputs
function updateManualCurrentPerformance() {
// Update current month
const monthsAgo = parseInt(currentMonthInput.value) || 0;
const now = new Date();
now.setMonth(now.getMonth() - monthsAgo);
const dateString = now.toLocaleDateString('default', { month: 'short', year: 'numeric' });
currentMonthEl.textContent = dateString;
// Update current production
const production = parseFloat(currentProductionInput.value);
if (!isNaN(production) && production >= 0) {
currentProductionEl.textContent = `${production.toFixed(2)} BOPD`;
} else {
currentProductionEl.textContent = "-";
}
}
function handleDragOver(e) {
e.preventDefault();
dropZone.classList.add('highlight');
}
function handleDragLeave(e) {
e.preventDefault();
dropZone.classList.remove('highlight');
}
function handleDrop(e) {
e.preventDefault();
dropZone.classList.remove('highlight');
const files = e.dataTransfer.files;
if (files.length) {
fileInput.files = files;
handleFileSelect({ target: fileInput });
}
}
function handleFileSelect(e) {
const file = e.target.files[0];
if (file) {
const fileName = file.name;
if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
uploadBtn.disabled = false;
dropZone.innerHTML = `
<i class="fas fa-file-excel text-5xl text-green-500 mb-4"></i>
<h3 class="text-xl font-medium text-gray-700 mb-2">${file.name}</h3>
<p class="text-sm text-gray-500">Ready to process</p>
`;
} else {
alert('Please upload a valid Excel file (.xlsx or .xls)');
uploadBtn.disabled = true;
}
}
}
async function processFile() {
const file = fileInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
// Convert to JSON
const jsonData = XLSX.utils.sheet_to_json(firstSheet);
// Parse and validate data
productionData = parseProductionData(jsonData);
if (productionData.length === 0) {
alert('No valid production data found. Please check your Excel file format.');
return;
}
// Sort by date
productionData.sort((a, b) => a.timeMonths - b.timeMonths);
// Set date filter min/max values based on data
setFilterDateBounds();
// Apply any existing filters
applyDateFilter();
// Make sure current performance info is updated
if (currentMonthInput.value || currentProductionInput.value) {
updateManualCurrentPerformance();
}
// Store source and display results
dataSource = 'upload';
displayResults();
// Analyze decline curve with selected model
currentAnalysis = performDeclineAnalysis(filteredData);
// Update analysis info
updateAnalysisInfo(currentAnalysis);
// Update current performance - this will prioritize data from Excel if available
updateCurrentPerformance();
// Create chart
createChart(filteredData, currentAnalysis);
// Show results section
resultsSection.classList.remove('hidden');
// Show active filters if any
updateActiveFiltersDisplay();
// Scroll to results
resultsSection.scrollIntoView({ behavior: 'smooth' });
} catch (error) {
console.error('Error processing file:', error);
alert('Error processing the Excel file. Please make sure it is a valid file.');
}
};
reader.readAsArrayBuffer(file);
}
function addManualDataPoint() {
const dateValue = productionDateInput.value;
const rateValue = productionRateInput.value;
const cumulativeValue = cumulativeInput.value;
if (!dateValue) {
alert('Please select a date');
productionDateInput.focus();
return;
}
if (!rateValue || parseFloat(rateValue) < 0) {
alert('Please enter a valid production rate');
productionRateInput.focus();
return;
}
const date = new Date(dateValue);
const rate = parseFloat(rateValue);
const cumulative = cumulativeValue ? parseFloat(cumulativeValue) : null;
// Create a new data point
const dataPoint = {
id: Date.now(), // Use timestamp as unique ID
date: date,
rate: rate,
cumulative: cumulative
};
// Add to manual data array
manualData.push(dataPoint);
// Sort data by date
manualData.sort((a, b) => a.date - b.date);
// Recalculate time months based on the first data point
if (manualData.length > 0) {
const firstDate = manualData[0].date;
manualData.forEach(point => {
point.timeMonths = (point.date - firstDate) / (1000 * 60 * 60 * 24 * 30.44);
point.timeMonths = parseFloat(point.timeMonths.toFixed(2));
});
}
// Update the data table
updateManualDataTable();
// Reset input fields
productionRateInput.value = '';
cumulativeInput.value = '';
// Set focus back to rate input for quick data entry
productionRateInput.focus();
// Update data count
updateDataCount();
// Flash feedback
addDataBtn.classList.add('flash');
setTimeout(() => {
addDataBtn.classList.remove('flash');
}, 500);
}
function updateManualDataTable() {
// Clear the table body
manualDataTableBody.innerHTML = '';
// Add each data point to the table
manualData.forEach(point => {
const row = document.createElement('tr');
row.setAttribute('data-id', point.id);
// Format the date
const formattedDate = point.date.toLocaleDateString();
// Create the row content
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap">
<div class="delete-btn" data-id="${point.id}">
<i class="fas fa-times"></i>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${formattedDate}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.rate.toFixed(2)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.cumulative ? point.cumulative.toFixed(0) : '-'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.timeMonths.toFixed(1)}</td>
`;
manualDataTableBody.appendChild(row);
});
// Add event listeners to delete buttons
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', function() {
const id = parseInt(this.getAttribute('data-id'));
deleteDataPoint(id);
});
});
}
function deleteDataPoint(id) {
// Remove the data point with the given ID
manualData = manualData.filter(point => point.id !== id);
// Recalculate time months based on the new first data point
if (manualData.length > 0) {
const firstDate = manualData[0].date;
manualData.forEach(point => {
point.timeMonths = (point.date - firstDate) / (1000 * 60 * 60 * 24 * 30.44);
point.timeMonths = parseFloat(point.timeMonths.toFixed(2));
});
}
// Update the table
updateManualDataTable();
// Update data count
updateDataCount();
// Flash feedback
const row = document.querySelector(`tr[data-id="${id}"]`);
if (row) {
row.style.backgroundColor = '#fee2e2';
setTimeout(() => {
row.style.backgroundColor = '';
}, 500);
}
}
function clearAllManualData() {
if (manualData.length === 0) return;
if (confirm('Are you sure you want to clear all manual data points?')) {
manualData = [];
updateManualDataTable();
updateDataCount();
// Flash feedback
clearAllBtn.classList.add('flash');
setTimeout(() => {
clearAllBtn.classList.remove('flash');
}, 500);
}
}
function updateDataCount() {
dataCount.textContent = `${manualData.length} entries`;
// Change color based on count
if (manualData.length === 0) {
dataCount.className = 'ml-2 bg-red-100 text-red-800 text-xs font-semibold px-2.5 py-0.5 rounded-full';
} else if (manualData.length < 3) {
dataCount.className = 'ml-2 bg-yellow-100 text-yellow-800 text-xs font-semibold px-2.5 py-0.5 rounded-full';
} else {
dataCount.className = 'ml-2 bg-green-100 text-green-800 text-xs font-semibold px-2.5 py-0.5 rounded-full';
}
}
function processManualData() {
if (manualData.length === 0) {
alert('Please add at least one data point before processing');
return;
}
// Use the manual data as production data
productionData = [...manualData];
// Sort by date
productionData.sort((a, b) => a.timeMonths - b.timeMonths);
// Set date filter min/max values based on data
setFilterDateBounds();
// Apply any existing filters
applyDateFilter();
// Make sure current performance info is updated
if (currentMonthInput.value || currentProductionInput.value) {
updateManualCurrentPerformance();
}
// Store source and display results
dataSource = 'manual';
displayResults();
// Analyze decline curve with selected model
currentAnalysis = performDeclineAnalysis(filteredData);
// Update analysis info
updateAnalysisInfo(currentAnalysis);
// Update current performance
updateCurrentPerformance();
// Create chart
createChart(filteredData, currentAnalysis);
// Show results section
resultsSection.classList.remove('hidden');
// Show active filters if any
updateActiveFiltersDisplay();
// Scroll to results
resultsSection.scrollIntoView({ behavior: 'smooth' });
}
function setFilterDateBounds() {
if ((dataSource === 'upload' ? productionData : manualData).length === 0) return;
const data = dataSource === 'upload' ? productionData : manualData;
// Set min/max for date filters based on data
const minDate = new Date(data[0].date);
const maxDate = new Date(data[data.length - 1].date);
startDateFilter.min = minDate.toISOString().split('T')[0];
startDateFilter.max = maxDate.toISOString().split('T')[0];
startDateFilter.value = minDate.toISOString().split('T')[0];
endDateFilter.min = minDate.toISOString().split('T')[0];
endDateFilter.max = maxDate.toISOString().split('T')[0];
endDateFilter.value = maxDate.toISOString().split('T')[0];
updateDateFilterStatus();
}
function applyDateFilter() {
if ((dataSource === 'upload' ? productionData : manualData).length === 0) return;
const data = dataSource === 'upload' ? productionData : manualData;
const start = startDateFilter.value ? new Date(startDateFilter.value) : null;
const end = endDateFilter.value ? new Date(endDateFilter.value) : null;
// Apply filters
filteredData = data.filter(point => {
const pointDate = new Date(point.date);
const afterStart = !start || pointDate >= start;
const beforeEnd = !end || pointDate <= end;
return afterStart && beforeEnd;
});
// Reset time months based on filtered data
if (filteredData.length > 0) {
const firstDate = new Date(filteredData[0].date);
filteredData.forEach(point => {
point.timeMonths = (new Date(point.date) - firstDate) / (1000 * 60 * 60 * 24 * 30.44);
point.timeMonths = parseFloat(point.timeMonths.toFixed(2));
});
}
// Update analysis if we have new filtered data
if (currentAnalysis && filteredData.length > 0) {
currentAnalysis = performDeclineAnalysis(filteredData);
updateAnalysisInfo(currentAnalysis);
createChart(filteredData, currentAnalysis);
}
// Update display results
displayResults();
updateActiveFiltersDisplay();
}
function updateDateFilterStatus() {
const start = startDateFilter.value;
const end = endDateFilter.value;
if (!start && !end) {
dateFilterStatus.textContent = "No date filters applied. Analyzing all available data.";
dateFilterStatus.className = "mt-2 text-sm text-gray-500";
} else if (start && end) {
dateFilterStatus.textContent = `Analyzing data from ${start} to ${end}.`;
dateFilterStatus.className = "mt-2 text-sm text-green-600 font-medium";
} else if (start) {
dateFilterStatus.textContent = `Analyzing data from ${start} onwards.`;
dateFilterStatus.className = "mt-2 text-sm text-blue-600 font-medium";
} else if (end) {
dateFilterStatus.textContent = `Analyzing data up to ${end}.`;
dateFilterStatus.className = "mt-2 text-sm text-blue-600 font-medium";
}
}
function updateActiveFiltersDisplay() {
activeFilters.innerHTML = '';
const start = startDateFilter.value;
const end = endDateFilter.value;
const model = declineModelSelect.options[declineModelSelect.selectedIndex].text;
// Add data source badge
const sourceBadge = document.createElement('div');
sourceBadge.className = 'filter-badge';
let sourceText = dataSource === 'manual' ? 'Manual Input' : dataSource === 'upload' ? 'Excel File' : 'No Data';
let sourceIcon = dataSource === 'manual' ? 'fa-keyboard' : dataSource === 'upload' ? 'fa-file-excel' : 'fa-exclamation-triangle';
sourceBadge.innerHTML = `<i class="fas ${sourceIcon}"></i> ${sourceText}`;
activeFilters.appendChild(sourceBadge);
// Add model filter badge
const modelBadge = document.createElement('div');
modelBadge.className = 'filter-badge';
modelBadge.innerHTML = `<i class="fas fa-cogs"></i> ${model}`;
activeFilters.appendChild(modelBadge);
// Add date range badges if filters are active
if (start) {
const startBadge = document.createElement('div');
startBadge.className = 'filter-badge';
startBadge.innerHTML = `<i class="fas fa-calendar"></i> From: ${new Date(start).toLocaleDateString()}`;
activeFilters.appendChild(startBadge);
}
if (end) {
const endBadge = document.createElement('div');
endBadge.className = 'filter-badge';
endBadge.innerHTML = `<i class="fas fa-calendar"></i> To: ${new Date(end).toLocaleDateString()}`;
activeFilters.appendChild(endBadge);
}
}
function parseProductionData(jsonData) {
const parsedData = [];
let startDate = null;
for (let row of jsonData) {
let date, rate, cumulative;
// Try to detect columns by common names
for (let key in row) {
const keyLower = key.toLowerCase();
if (date === undefined && (keyLower.includes('date') || keyLower.includes('time'))) {
date = new Date(row[key]);
}
if (rate === undefined && (keyLower.includes('rate') || keyLower.includes('oil') || keyLower.includes('prod'))) {
rate = parseFloat(row[key]);
}
if (cumulative === undefined && (keyLower.includes('cum') || keyLower.includes('total') || keyLower.includes('cumulative'))) {
cumulative = parseFloat(row[key]);
}
}
// Fallback: assume first three columns are date, rate, cumulative if no headers
if (parsedData.length === 0 && Object.keys(row).length === 3 && date === undefined) {
const values = Object.values(row);
date = new Date(values[0]);
rate = parseFloat(values[1]);
cumulative = parseFloat(values[2]);
}
// Validate data
if (date && !isNaN(date.getTime()) && !isNaN(rate) && rate >= 0) {
// Set start date for time calculations
if (!startDate) {
startDate = new Date(date);
}
// Calculate time in months from start
const timeMonths = (date - startDate) / (1000 * 60 * 60 * 24 * 30.44);
parsedData.push({
date: new Date(date),
rate: rate,
cumulative: !isNaN(cumulative) ? cumulative : null,
timeMonths: parseFloat(timeMonths.toFixed(2))
});
}
}
return parsedData;
}
function updateAnalysisParameters() {
// Update b factor based on selected model
switch(declineModelSelect.value) {
case 'exponential':
currentAnalysis.b = 0;
break;
case 'harmonic':
currentAnalysis.b = 1;
break;
case 'hyperbolic':
// Keep calculated value or default to 0.5
if (currentAnalysis.b === 0 || currentAnalysis.b === 1) {
currentAnalysis.b = 0.5;
}
break;
}
}
function performDeclineAnalysis(data) {
// Simple decline curve analysis using hyperbolic decline
// q(t) = qi / (1 + b * Di * t)^(1/b)
if (data.length === 0) return null;
const qi = data[0].rate; // Initial rate from first point in filtered data
const productionMonths = data.length > 1 ? (data[data.length - 1].timeMonths - data[0].timeMonths) : 0;
// Simple estimation of decline parameters
let Di = 0; // Nominal decline rate
let b = 0.5; // Decline exponent (b=0 exponential, b=1 harmonic, 0<b<1 hyperbolic)
let rSquared = 0;
// Calculate nominal decline rate based on first and last points
if (data.length > 1 && productionMonths > 0) {
const qf = data[data.length - 1].rate;
// For hyperbolic decline, we'll make a simple estimation
const avgDecline = (qi - qf) / qi / (productionMonths/12); // Annual decline rate
Di = Math.abs(avgDecline);
// Adjust b factor based on the shape of the decline
const declineRatio = qf / qi;
if (declineRatio > 0.5) {
b = 0.3; // Steeper decline
} else if (declineRatio > 0.3) {
b = 0.5;
} else {
b = 0.8; // Gentle decline
}
} else {
Di = 0.2; // Default 20% annual decline
b = 0.5;
}
// Apply model-specific constraints
switch(declineModelSelect.value) {
case 'exponential':
b = 0;
break;
case 'harmonic':
b = 1;
break;
}
// Calculate R-squared (very simplified)
let ssRes = 0;
let ssTot = 0;
const yMean = data.reduce((sum, point) => sum + point.rate, 0) / data.length;
data.forEach(point => {
const predicted = predictRate(point.timeMonths, qi, Di, b);
ssRes += Math.pow(point.rate - predicted, 2);
ssTot += Math.pow(point.rate - yMean, 2);
});
rSquared = ssTot > 0 ? 1 - (ssRes / ssTot) : 0;
return { qi, Di, b, rSquared };
}
function predictRate(time, qi, Di, b) {
// Hyperbolic decline model
// q(t) = qi / (1 + b * Di * t)^(1/b)
// Time is in years for standard decline parameters
const timeYears = time / 12;
if (b === 0) {
// Exponential decline
return qi * Math.exp(-Di * timeYears);
} else if (Math.abs(b - 1) < 0.01) {
// Harmonic decline
return qi / (1 + Di * timeYears);
} else {
// Hyperbolic decline
const base = 1 + b * Di * timeYears;
return base > 0 ? qi / Math.pow(base, 1/b) : 0;
}
}
function updateCurrentPerformance() {
if ((dataSource === 'upload' ? productionData : manualData).length === 0) return;
const data = dataSource === 'upload' ? productionData : manualData;
// Get the latest data point
const latestPoint = data[data.length - 1];
// Update current month
currentMonthEl.textContent = latestPoint.date.toLocaleDateString('default', { month: 'short', year: 'numeric' });
// Update current production
currentProductionEl.textContent = `${latestPoint.rate.toFixed(2)} BOPD`;
// Update cumulative production
if (latestPoint.cumulative) {
cumulativeProductionEl.textContent = `${Math.round(latestPoint.cumulative).toLocaleString()} STB`;
} else {
// If cumulative values aren't provided, estimate based on trapezoidal rule
let estimatedCumulative = 0;
for (let i = 1; i < data.length; i++) {
const dt = (data[i].timeMonths - data[i-1].timeMonths) * 30.44; // days
const avgRate = (data[i].rate + data[i-1].rate) / 2;
estimatedCumulative += avgRate * dt;
}
cumulativeProductionEl.textContent = `${Math.round(estimatedCumulative).toLocaleString()} STB (estimated)`;
}
// Calculate and display production life
const productionLifeInMonths = latestPoint.timeMonths;
const years = Math.floor(productionLifeInMonths / 12);
const months = Math.round(productionLifeInMonths % 12);
if (years > 0) {
productionLifeEl.textContent = `${years}y ${months}m`;
} else {
productionLifeEl.textContent = `${months} months`;
}
}
function displayResults() {
// Determine which data to use (filtered or original)
const dataToUse = filteredData.length > 0 ? filteredData :
(dataSource === 'upload' ? productionData : manualData);
// Populate table
const tableBody = document.getElementById('tableBody');
tableBody.innerHTML = '';
if (dataToUse.length === 0) return;
dataToUse.forEach(point => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.date.toLocaleDateString()}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.rate.toFixed(2)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.cumulative ? point.cumulative.toFixed(0) : '-'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.timeMonths.toFixed(1)}</td>
`;
tableBody.appendChild(row);
});
}
function updateAnalysisInfo(analysis) {
if (!analysis) return;
document.getElementById('qiValue').textContent = `${analysis.qi.toFixed(2)} BOPD`;
document.getElementById('diValue').textContent = `${(analysis.Di * 100).toFixed(1)}%/yr`;
document.getElementById('bValue').textContent = analysis.b.toFixed(2);
document.getElementById('r2Value').textContent = analysis.rSquared.toFixed(3);
}
function createChart(data, analysis) {
if (!analysis || data.length === 0) return;
const ctx = document.getElementById('productionChart').getContext('2d');
// Destroy existing chart if it exists
if (productionChart) {
productionChart.destroy();
}
const actualTime = data.map(d => d.timeMonths);
const actualRates = data.map(d => d.rate);
// Generate forecast data
const maxTime = Math.max(...actualTime);
const forecastTime = [];
const forecastRates = [];
const forecastPeriod = parseInt(forecastMonths.value);
const b = analysis.b; // Use the b value from analysis which is set by the model selection
// Create data points for the chart
for (let t = 0; t <= maxTime + forecastPeriod; t += 0.5) {
forecastTime.push(t);
forecastRates.push(predictRate(t, analysis.qi, analysis.Di, b));
}
productionChart = new Chart(ctx, {
type: 'line',
data: {
labels: forecastTime,
datasets: [
{
label: 'Historical Production',
data: actualRates,
borderColor: '#4c1d95',
backgroundColor: 'rgba(76, 29, 149, 0.1)',
borderWidth: 3,
pointBackgroundColor: '#4c1d95',
pointRadius: 4,
pointHoverRadius: 6,
fill: false,
tension: 0.2
},
{
label: 'Forecasted Decline',
data: forecastRates,
borderColor: '#059669',
borderDash: [10, 5],
borderWidth: 3,
pointBackgroundColor: '#059669',
pointRadius: 0,
fill: false,
tension: 0.2
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#ffffff',
bodyColor: '#ffffff',
borderColor: '#ffffff',
borderWidth: 1,
cornerRadius: 8,
displayColors: true,
callbacks: {
title: function(tooltipItems) {
const timeMonths = tooltipItems[0].label;
const years = Math.floor(timeMonths / 12);
const months = Math.round(timeMonths % 12);
return `Time: ${years}y ${months}m`;
},
label: function(context) {
return `${context.dataset.label}: ${context.parsed.y.toFixed(2)} BOPD`;
}
}
},
legend: {
position: 'top',
labels: {
usePointStyle: true,
padding: 20,
font: {
size: 14,
weight: 500
}
}
},
title: {
display: false
}
},
scales: {
x: {
type: 'linear',
position: 'bottom',
title: {
display: true,
text: 'Time (months)',
font: {
size: 14,
weight: 500
}
},
grid: {
color: 'rgba(0, 0, 0, 0.05)'
}
},
y: {
title: {
display: true,
text: 'Production Rate (BOPD)',
font: {
size: 14,
weight: 500
}
},
grid: {
color: 'rgba(0, 0, 0, 0.05)'
},
beginAtZero: true
}
}
}
});
}
function updateChart() {
if (filteredData.length > 0 ? filteredData :
(dataSource === 'upload' ? productionData : manualData).length > 0 && currentAnalysis) {
updateAnalysisParameters();
currentAnalysis = performDeclineAnalysis(filteredData.length > 0 ? filteredData :
(dataSource === 'upload' ? productionData : manualData));
updateAnalysisInfo(currentAnalysis);
createChart(filteredData.length > 0 ? filteredData :
(dataSource === 'upload' ? productionData : manualData), currentAnalysis);
}
}
function exportResults() {
// In case the user didn't update the manual inputs, make sure current performance is set
if (!currentProductionEl.textContent || currentProductionEl.textContent === "-") {
updateManualCurrentPerformance();
}
// Create a worksheet
const dataToUse = filteredData.length > 0 ? filteredData :
(dataSource === 'upload' ? productionData : manualData);
const ws_data = [
['Date', 'Rate (BOPD)', 'Cumulative (STB)', 'Time (months)'],
...dataToUse.map(d => [d.date.toLocaleDateString(), d.rate, d.cumulative, d.timeMonths])
];
const ws = XLSX.utils.aoa_to_sheet(ws_data);
// Add analysis results
XLSX.utils.sheet_add_aoa(ws, [
['', ''], // blank row
['Analysis Results', ''],
['Initial Rate (qi)', document.getElementById('qiValue').textContent],
['Decline Rate (Di)', document.getElementById('diValue').textContent],
['Decline Exponent (b)', document.getElementById('bValue').textContent],
['R² Fit', document.getElementById('r2Value').textContent],
['Production Life', document.getElementById('productionLife').textContent],
['Current Month', currentMonthEl.textContent],
['Current Production', currentProductionEl.textContent],
['Cumulative Production', cumulativeProductionEl.textContent],
['Data Source', dataSource === 'manual' ? 'Manual Input' : 'Uploaded Excel File'],
['Model Used', declineModelSelect.options[declineModelSelect.selectedIndex].text],
['Date Range', startDateFilter.value ? (endDateFilter.value ? `${startDateFilter.value} to ${endDateFilter.value}` : `From ${startDateFilter.value}`) : (endDateFilter.value ? `To ${endDateFilter.value}` : 'All data')],
['Forecast Period', `${forecastMonths.value} months`]
], { origin: -1 });
// Create workbook and download
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Production Data');
// Generate file name
const dateStr = new Date().toISOString().slice(0, 10);
const source = dataSource === 'manual' ? 'Manual' : 'Excel';
XLSX.writeFile(wb, `DeclineCurveAnalysis_${source}_${dateStr}.xlsx`);
}
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-qwensite.hf.space/logo.svg" alt="qwensite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-qwensite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >QwenSite</a> - 🧬 <a href="https://enzostvs-qwensite.hf.space?remix=alterzick/dca" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>