feat: add job maintenance dashboard and automated stale job expiration system
This commit is contained in:
@@ -102,7 +102,11 @@ const mainLayoutHTML = `{{ define "page" }}<!DOCTYPE html>
|
||||
</button>
|
||||
<button id="tab-btn-targets" onclick="switchTab('targets')"
|
||||
class="tab-btn px-5 py-3 text-sm font-semibold border-b-2 transition-colors text-slate-400 border-transparent hover:text-slate-200">
|
||||
⚙️ Targets
|
||||
🎯 Targets
|
||||
</button>
|
||||
<button id="tab-btn-preferences" onclick="switchTab('preferences')"
|
||||
class="tab-btn px-5 py-3 text-sm font-semibold border-b-2 transition-colors text-slate-400 border-transparent hover:text-slate-200">
|
||||
⚙️ Preferences
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,6 +130,11 @@ const mainLayoutHTML = `{{ define "page" }}<!DOCTYPE html>
|
||||
{{ template "targetsTab" . }}
|
||||
</div>
|
||||
|
||||
<!-- Tab: Preferences -->
|
||||
<div id="tab-content-preferences" class="hidden space-y-5">
|
||||
{{ template "preferencesTab" . }}
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Right Slide-Over Detail Panel -->
|
||||
@@ -230,19 +239,25 @@ const mainLayoutHTML = `{{ define "page" }}<!DOCTYPE html>
|
||||
|
||||
<script>
|
||||
// ── Tab Routing ──────────────────────────────────────────────────────
|
||||
const TABS = ['jobs', 'apply', 'targets'];
|
||||
const TABS = ['jobs', 'apply', 'targets', 'preferences'];
|
||||
|
||||
function switchTab(tab) {
|
||||
TABS.forEach(t => {
|
||||
document.getElementById('tab-content-' + t).classList.add('hidden');
|
||||
const content = document.getElementById('tab-content-' + t);
|
||||
if (content) content.classList.add('hidden');
|
||||
const btn = document.getElementById('tab-btn-' + t);
|
||||
btn.classList.remove('text-indigo-400', 'border-indigo-500');
|
||||
btn.classList.add('text-slate-400', 'border-transparent');
|
||||
if (btn) {
|
||||
btn.classList.remove('text-indigo-400', 'border-indigo-500');
|
||||
btn.classList.add('text-slate-400', 'border-transparent');
|
||||
}
|
||||
});
|
||||
document.getElementById('tab-content-' + tab).classList.remove('hidden');
|
||||
const activeContent = document.getElementById('tab-content-' + tab);
|
||||
if (activeContent) activeContent.classList.remove('hidden');
|
||||
const activeBtn = document.getElementById('tab-btn-' + tab);
|
||||
activeBtn.classList.remove('text-slate-400', 'border-transparent');
|
||||
activeBtn.classList.add('text-indigo-400', 'border-indigo-500');
|
||||
if (activeBtn) {
|
||||
activeBtn.classList.remove('text-slate-400', 'border-transparent');
|
||||
activeBtn.classList.add('text-indigo-400', 'border-indigo-500');
|
||||
}
|
||||
history.replaceState(null, '', '#' + tab);
|
||||
}
|
||||
|
||||
@@ -308,6 +323,49 @@ const mainLayoutHTML = `{{ define "page" }}<!DOCTYPE html>
|
||||
});
|
||||
}
|
||||
|
||||
// ── Maintenance Actions ──────────────────────────────────────────────
|
||||
function runMaintenanceAction(url, daysInputId, protectCheckboxId, btn, originalText) {
|
||||
const days = document.getElementById(daysInputId)?.value || '14';
|
||||
const protect = document.getElementById(protectCheckboxId)?.checked ?? true;
|
||||
const alertBox = document.getElementById('maintenance-alert');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Processing…';
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ days: days, protect: protect ? 'true' : 'false' })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (alertBox) {
|
||||
alertBox.className = 'p-4 rounded-xl border text-xs font-semibold ' +
|
||||
(data.status === 'success' ? 'bg-emerald-950/60 border-emerald-500/40 text-emerald-300' : 'bg-rose-950/60 border-rose-500/40 text-rose-300');
|
||||
alertBox.innerHTML = '<span>✓</span> ' + (data.message || 'Operation completed successfully.');
|
||||
alertBox.classList.remove('hidden');
|
||||
setTimeout(() => { alertBox.classList.add('hidden'); }, 8000);
|
||||
}
|
||||
// Refresh Jobs and Apply tables & stats
|
||||
const jobsContainer = document.getElementById('jobs-container');
|
||||
if (jobsContainer) htmx.trigger(jobsContainer, 'refresh');
|
||||
const applyContainer = document.getElementById('apply-container');
|
||||
if (applyContainer) htmx.trigger(applyContainer, 'refresh');
|
||||
refreshStatus();
|
||||
})
|
||||
.catch(err => {
|
||||
if (alertBox) {
|
||||
alertBox.className = 'p-4 rounded-xl border text-xs font-semibold bg-rose-950/60 border-rose-500/40 text-rose-300';
|
||||
alertBox.innerHTML = '<span>✕</span> Error: ' + err.message;
|
||||
alertBox.classList.remove('hidden');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Bottom Bar: Status Polling ────────────────────────────────────────
|
||||
function refreshStatus() {
|
||||
fetch('/api/status')
|
||||
@@ -392,14 +450,15 @@ const mainLayoutHTML = `{{ define "page" }}<!DOCTYPE html>
|
||||
table.querySelectorAll('tr[data-row]').forEach(row => {
|
||||
const rowText = row.textContent.toLowerCase();
|
||||
const rowQual = row.dataset.qual || ''; // qualified | not-qualified | pending
|
||||
const rowApp = row.dataset.app || 'none'; // none | opted-out | pipeline
|
||||
const rowApp = row.dataset.app || 'none'; // none | opted-out | expired | pipeline
|
||||
const textMatch = !text || rowText.includes(text);
|
||||
let qualMatch;
|
||||
switch (qual) {
|
||||
case 'default': qualMatch = rowApp !== 'opted-out' && rowApp !== 'pipeline' && (rowQual === 'qualified' || rowQual === 'pending'); break;
|
||||
case 'not-qualified': qualMatch = rowApp !== 'opted-out' && rowQual === 'not-qualified'; break;
|
||||
case 'default': qualMatch = rowApp !== 'opted-out' && rowApp !== 'expired' && rowApp !== 'pipeline' && (rowQual === 'qualified' || rowQual === 'pending'); break;
|
||||
case 'not-qualified': qualMatch = rowApp !== 'opted-out' && rowApp !== 'expired' && rowQual === 'not-qualified'; break;
|
||||
case 'saved': qualMatch = rowApp === 'pipeline'; break;
|
||||
case 'all': qualMatch = rowApp !== 'opted-out'; break;
|
||||
case 'all': qualMatch = true; break;
|
||||
case 'expired': qualMatch = rowApp === 'expired'; break;
|
||||
case 'opted-out': qualMatch = rowApp === 'opted-out'; break;
|
||||
default: qualMatch = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user