first commit

This commit is contained in:
bolade
2025-08-05 22:29:54 +01:00
commit 974ffa6554
33 changed files with 3297 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Email Alerts System{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.sidebar {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.sidebar .nav-link {
color: rgba(255,255,255,0.8);
border-radius: 8px;
margin: 2px 0;
transition: all 0.3s ease;
}
.sidebar .nav-link:hover {
color: white;
background-color: rgba(255,255,255,0.1);
}
.sidebar .nav-link.active {
background-color: rgba(255,255,255,0.2);
color: white;
}
.main-content {
background-color: #f8f9fa;
min-height: 100vh;
}
.card {
border: none;
border-radius: 15px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: transform 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
}
.btn-primary:hover {
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
.alert {
border-radius: 10px;
border: none;
}
.table {
border-radius: 10px;
overflow: hidden;
}
.form-control, .form-select {
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.form-control:focus, .form-select:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<div class="col-md-3 col-lg-2 px-0">
<div class="sidebar p-3">
<div class="text-center mb-4">
<h4 class="text-white">
<i class="fas fa-envelope-open-text me-2"></i>
Email Alerts
</h4>
</div>
<nav class="nav flex-column">
<a class="nav-link {% if request.endpoint == 'index' %}active{% endif %}" href="{{ url_for('index') }}">
<i class="fas fa-tachometer-alt me-2"></i>
Dashboard
</a>
<a class="nav-link {% if request.endpoint == 'settings' %}active{% endif %}" href="{{ url_for('settings') }}">
<i class="fas fa-cog me-2"></i>
Settings
</a>
</nav>
</div>
</div>
<!-- Main Content -->
<div class="col-md-9 col-lg-10">
<div class="main-content p-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'success' if category == 'success' else 'danger' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
+320
View File
@@ -0,0 +1,320 @@
{% extends "base.html" %}
{% block title %}Dashboard - Email Alerts System{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<h2 class="mb-4">
<i class="fas fa-tachometer-alt me-2"></i>
Dashboard
</h2>
</div>
</div>
<!-- Status Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-envelope fa-2x text-primary mb-2"></i>
<h5 class="card-title">Email Address</h5>
<p class="card-text">{{ config.email_address }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-clock fa-2x text-warning mb-2"></i>
<h5 class="card-title">Time Frames</h5>
<p class="card-text">{{ config.time_frames|length }} configured</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-calendar-day fa-2x text-info mb-2"></i>
<h5 class="card-title">Email Range</h5>
<p class="card-text">Last {{ config.email_days_back }} days</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-building fa-2x text-success mb-2"></i>
<h5 class="card-title">Agency Domains</h5>
<p class="card-text">{{ config.agency_domains|length }} domains</p>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<h5 class="card-title mb-3">
<i class="fas fa-play-circle me-2"></i>
System Actions
</h5>
<div class="row">
<div class="col-md-4">
<button class="btn btn-primary w-100 mb-2" onclick="testConnection()">
<i class="fas fa-wifi me-2"></i>
Test Connection
</button>
</div>
<div class="col-md-4">
<button class="btn btn-success w-100 mb-2" onclick="processEmails()">
<i class="fas fa-envelope-open me-2"></i>
Process Emails
</button>
</div>
<div class="col-md-4">
<button class="btn btn-info w-100 mb-2" onclick="refreshThreads()">
<i class="fas fa-sync-alt me-2"></i>
Refresh Threads
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Results Section -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<h5 class="card-title mb-3">
<i class="fas fa-list me-2"></i>
Processing Results
</h5>
<div id="results-container">
<div class="text-center text-muted">
<i class="fas fa-info-circle fa-2x mb-2"></i>
<p>Click "Process Emails" to start processing and view results here.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Threads Table -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<h5 class="card-title mb-3">
<i class="fas fa-exclamation-triangle me-2"></i>
Threads Needing Alerts
</h5>
<div id="threads-container">
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading threads...</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function testConnection() {
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Testing...';
button.disabled = true;
fetch('/test_connection')
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
showAlert('success', data.message);
} else {
showAlert('danger', data.message);
}
})
.catch(error => {
showAlert('danger', 'Connection test failed: ' + error.message);
})
.finally(() => {
button.innerHTML = originalText;
button.disabled = false;
});
}
function processEmails() {
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Processing...';
button.disabled = true;
fetch('/process_emails', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
showAlert('success', data.message);
updateResults(data.data);
} else {
showAlert('danger', data.message);
}
})
.catch(error => {
showAlert('danger', 'Processing failed: ' + error.message);
})
.finally(() => {
button.innerHTML = originalText;
button.disabled = false;
});
}
function refreshThreads() {
const container = document.getElementById('threads-container');
container.innerHTML = `
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading threads...</p>
</div>
`;
fetch('/get_threads')
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
updateThreadsTable(data.threads);
} else {
container.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
${data.message}
</div>
`;
}
})
.catch(error => {
container.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
Error loading threads: ${error.message}
</div>
`;
});
}
function updateResults(data) {
const container = document.getElementById('results-container');
container.innerHTML = `
<div class="row">
<div class="col-md-4">
<div class="text-center">
<h4 class="text-primary">${data.total_emails}</h4>
<p class="text-muted">Total Emails</p>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<h4 class="text-warning">${data.actionable_emails}</h4>
<p class="text-muted">Actionable Emails</p>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<h4 class="text-success">${data.sent_alerts}</h4>
<p class="text-muted">Alerts Sent</p>
</div>
</div>
</div>
`;
}
function updateThreadsTable(threads) {
const container = document.getElementById('threads-container');
if (threads.length === 0) {
container.innerHTML = `
<div class="text-center text-muted">
<i class="fas fa-check-circle fa-2x mb-2"></i>
<p>No threads currently need alerts.</p>
</div>
`;
return;
}
let tableHtml = `
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Subject</th>
<th>Last Message</th>
<th>Hours Since</th>
<th>Alert Level</th>
</tr>
</thead>
<tbody>
`;
threads.forEach(thread => {
const alertClass = thread.alert_level === 3 ? 'danger' :
thread.alert_level === 2 ? 'warning' : 'info';
const alertText = thread.alert_level === 3 ? 'CRITICAL' :
thread.alert_level === 2 ? 'URGENT' : 'NORMAL';
tableHtml += `
<tr>
<td><strong>${thread.subject}</strong></td>
<td>${thread.last_message}</td>
<td>${thread.hours_since} hours</td>
<td><span class="badge bg-${alertClass}">${alertText}</span></td>
</tr>
`;
});
tableHtml += `
</tbody>
</table>
</div>
`;
container.innerHTML = tableHtml;
}
function showAlert(type, message) {
const alertHtml = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
const container = document.querySelector('.main-content');
const alertDiv = document.createElement('div');
alertDiv.innerHTML = alertHtml;
container.insertBefore(alertDiv.firstElementChild, container.firstChild);
}
// Load threads on page load
document.addEventListener('DOMContentLoaded', function() {
refreshThreads();
});
</script>
{% endblock %}
+265
View File
@@ -0,0 +1,265 @@
{% extends "base.html" %}
{% block title %}Settings - Email Alerts System{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<h2 class="mb-4">
<i class="fas fa-cog me-2"></i>
System Settings
</h2>
</div>
</div>
<form method="POST" action="{{ url_for('update_settings') }}">
<div class="row">
<!-- Email Configuration -->
<div class="col-md-6">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-envelope me-2"></i>
Email Configuration
</h5>
<div class="mb-3">
<label for="email_address" class="form-label">Email Address to Monitor</label>
<input type="email" class="form-control" id="email_address" name="email_address"
value="{{ config.email_address }}" required>
<div class="form-text">The email address that will be checked for new messages.</div>
</div>
<div class="mb-3">
<label for="zoho_email" class="form-label">Zoho Email Address</label>
<input type="email" class="form-control" id="zoho_email" name="zoho_email"
value="{{ config.zoho_email }}" required>
<div class="form-text">Your Zoho email address for IMAP access.</div>
</div>
<div class="mb-3">
<label for="zoho_app_password" class="form-label">Zoho App Password</label>
<input type="password" class="form-control" id="zoho_app_password" name="zoho_app_password"
value="{{ config.zoho_app_password }}" required>
<div class="form-text">App password for Zoho IMAP access (not your regular password).</div>
</div>
<div class="mb-3">
<label for="email_days_back" class="form-label">Email Range (Days)</label>
<input type="number" class="form-control" id="email_days_back" name="email_days_back"
value="{{ config.email_days_back }}" min="1" max="365" required>
<div class="form-text">How many days back to check for emails (1-365 days).</div>
</div>
<div class="mb-3">
<label for="agency_domains" class="form-label">Agency Domains</label>
<textarea class="form-control" id="agency_domains" name="agency_domains" rows="3"
placeholder="projects@manaknightdigital.com, support@company.com">{{ config.agency_domains|join(', ') }}</textarea>
<div class="form-text">Comma-separated list of email domains that indicate agency responses.</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="auto_process" name="auto_process"
{% if config.auto_process %}checked{% endif %}>
<label class="form-check-label" for="auto_process">
Enable Automatic Email Processing
</label>
</div>
<div class="form-text">Automatically process emails at regular intervals.</div>
</div>
<div class="mb-3">
<label for="auto_process_interval" class="form-label">Processing Interval (minutes)</label>
<input type="number" class="form-control" id="auto_process_interval" name="auto_process_interval"
value="{{ config.auto_process_interval }}" min="5" max="1440">
<div class="form-text">How often to automatically process emails (5-1440 minutes).</div>
</div>
</div>
</div>
</div>
<!-- Time Frames Configuration -->
<div class="col-md-6">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-clock me-2"></i>
Alert Time Frames
</h5>
<p class="text-muted">Configure when alerts should be sent based on response time.</p>
<div id="time-frames-container">
{% for frame in config.time_frames %}
<div class="time-frame-row mb-3 p-3 border rounded">
<div class="row">
<div class="col-md-4">
<label class="form-label">Name</label>
<input type="text" class="form-control" name="frame_name[]"
value="{{ frame.name }}" placeholder="e.g., 1-24 hours">
</div>
<div class="col-md-3">
<label class="form-label">Hours</label>
<input type="number" class="form-control" name="frame_hours[]"
value="{{ frame.hours }}" min="1" max="720">
</div>
<div class="col-md-3">
<label class="form-label">Alert Level</label>
<select class="form-select" name="frame_level[]">
<option value="1" {% if frame.alert_level == 1 %}selected{% endif %}>Level 1 (Normal)</option>
<option value="2" {% if frame.alert_level == 2 %}selected{% endif %}>Level 2 (Urgent)</option>
<option value="3" {% if frame.alert_level == 3 %}selected{% endif %}>Level 3 (Critical)</option>
</select>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeTimeFrame(this)">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
{% endfor %}
</div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addTimeFrame()">
<i class="fas fa-plus me-2"></i>
Add Time Frame
</button>
</div>
</div>
</div>
</div>
<!-- Save Button -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">
<i class="fas fa-save me-2"></i>
Save Configuration
</h6>
<small class="text-muted">Click save to update all settings</small>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
Save Settings
</button>
</div>
</div>
</div>
</div>
</div>
</form>
<!-- Configuration Preview -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-eye me-2"></i>
Current Configuration Preview
</h5>
<div class="row">
<div class="col-md-6">
<h6>Email Settings</h6>
<ul class="list-unstyled">
<li><strong>Email:</strong> <span id="preview-email">{{ config.email_address }}</span></li>
<li><strong>Range:</strong> <span id="preview-range">{{ config.email_days_back }}</span> days</li>
<li><strong>Domains:</strong> <span id="preview-domains">{{ config.agency_domains|join(', ') }}</span></li>
<li><strong>Auto Processing:</strong> <span id="preview-auto">{{ 'Enabled' if config.auto_process else 'Disabled' }}</span></li>
<li><strong>Interval:</strong> <span id="preview-interval">{{ config.auto_process_interval }}</span> minutes</li>
</ul>
</div>
<div class="col-md-6">
<h6>Time Frames</h6>
<div id="preview-frames">
{% for frame in config.time_frames %}
<div class="mb-1">
<span class="badge bg-primary me-2">{{ frame.name }}</span>
<small>{{ frame.hours }} hours (Level {{ frame.alert_level }})</small>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function addTimeFrame() {
const container = document.getElementById('time-frames-container');
const newFrame = document.createElement('div');
newFrame.className = 'time-frame-row mb-3 p-3 border rounded';
newFrame.innerHTML = `
<div class="row">
<div class="col-md-4">
<label class="form-label">Name</label>
<input type="text" class="form-control" name="frame_name[]"
placeholder="e.g., 1-24 hours">
</div>
<div class="col-md-3">
<label class="form-label">Hours</label>
<input type="number" class="form-control" name="frame_hours[]"
value="24" min="1" max="720">
</div>
<div class="col-md-3">
<label class="form-label">Alert Level</label>
<select class="form-select" name="frame_level[]">
<option value="1">Level 1 (Normal)</option>
<option value="2">Level 2 (Urgent)</option>
<option value="3">Level 3 (Critical)</option>
</select>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeTimeFrame(this)">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
container.appendChild(newFrame);
}
function removeTimeFrame(button) {
const frameRow = button.closest('.time-frame-row');
frameRow.remove();
}
// Update preview when form fields change
document.addEventListener('DOMContentLoaded', function() {
const emailInput = document.getElementById('email_address');
const rangeInput = document.getElementById('email_days_back');
const domainsInput = document.getElementById('agency_domains');
const autoProcessInput = document.getElementById('auto_process');
const intervalInput = document.getElementById('auto_process_interval');
emailInput.addEventListener('input', function() {
document.getElementById('preview-email').textContent = this.value;
});
rangeInput.addEventListener('input', function() {
document.getElementById('preview-range').textContent = this.value;
});
domainsInput.addEventListener('input', function() {
document.getElementById('preview-domains').textContent = this.value;
});
autoProcessInput.addEventListener('change', function() {
document.getElementById('preview-auto').textContent = this.checked ? 'Enabled' : 'Disabled';
});
intervalInput.addEventListener('input', function() {
document.getElementById('preview-interval').textContent = this.value;
});
});
</script>
{% endblock %}