Files
job-dashboard/index.html

402 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Job Tracker · Anthony</title>
<script>
const API_BASE = (window.location.pathname.replace(/\/index\.html$/,'').replace(/\/+$/,'')) || '';
const API = { jobs:API_BASE+'/api/jobs', stats:API_BASE+'/api/stats', refresh:API_BASE+'/api/refresh', update:(id)=>API_BASE+'/api/jobs/'+id+'/update', bulk:API_BASE+'/api/jobs/bulk-update', clRegen:(id)=>API_BASE+'/api/jobs/'+id+'/cover-letter/regenerate', marketIntel:API_BASE+'/api/market-intel' };
const STATUSES = ['Backlog','Applied','Phone Screen','Interview','Offer','Accepted','Rejected','Ghosted','Declined'];
const PRIOS = ['','Low','Medium','High','Urgent'];
const SCOLORS = { 'Backlog':'#64748b','Applied':'#3b82f6','Phone Screen':'#8b5cf6','Interview':'#f59e0b','Offer':'#22c55e','Accepted':'#16a34a','Rejected':'#ef4444','Ghosted':'#78716c','Declined':'#6b7280' };
const SORDER = ['Backlog','Applied','Phone Screen','Interview','Offer','Accepted','Rejected','Ghosted','Declined'];
let state = { jobs:[], stats:{}, marketIntel:null, filtered:[], selected:new Set(), filter:'all', search:'', sort:'date-desc', view:'table' };
function notify(m,t) {
const e=document.getElementById('n');
e.textContent=m; e.className='n '+(t||'ok'); e.style.display='block';
setTimeout(()=>e.style.display='none',3500);
}
async function f(url,opts) {
try { const r=await fetch(url,{headers:{'Content-Type':'application/json'},...opts}); return await r.json(); }
catch(e){ notify('API error: '+e.message,'err'); return null; }
}
function computeMatchScore(j) {
let score=50;
if(j.strong_match) score+=20;
if(j.salary) score+=5;
if(j.notes&&j.notes.length>50) score+=10; else if(j.notes) score+=5;
if(j.cover_letter_url) score+=10;
if(j.role&&j.name) score+=5;
if(j.url) score+=5;
return Math.min(score,99);
}
function matchColor(s){ return s>=80?'#22c55e':s>=60?'#22d3ee':s>=40?'#f59e0b':'#ef4444'; }
async function load() {
const [jd,sd,mid]=await Promise.all([f(API.jobs,{}),f(API.stats,{}),f(API.marketIntel,{})]);
if(jd&&jd.jobs) state.jobs=jd.jobs;
if(sd) state.stats=sd;
if(mid) state.marketIntel=mid;
filter(); render();
}
async function upd(id,up) { const r=await f(API.update(id),{method:'POST',body:JSON.stringify(up)}); if(r&&r.ok){notify('Saved','ok');load();}else notify('Save failed','err'); }
async function bulk(ids,up) { if(!ids.length)return; const r=await f(API.bulk,{method:'POST',body:JSON.stringify({ids,updates:up})}); if(r&&r.ok){notify('Updated '+(r.results?r.results.filter(x=>x.ok).length+' jobs':'')+'','ok');load();} }
async function refresh() { const r=await f(API.refresh,{method:'POST'}); if(r&&r.ok){notify('Refreshed — '+r.total+' jobs','ok');load();} }
function filter() {
let fi=[...state.jobs];
if(state.filter==='__urgent'){ fi=fi.filter(j=>j.closing_status==='urgent'); }
else if(state.filter==='__past'){ fi=fi.filter(j=>j.closing_status==='past'); }
else if(state.filter==='__strong'){ fi=fi.filter(j=>j.strong_match); }
else if(state.filter!=='all') fi=fi.filter(j=>j.status===state.filter);
if(state.search){ const q=state.search.toLowerCase(); fi=fi.filter(j=>(j.name+(j.role||'')+(j.location||'')+(j.notes||'')).toLowerCase().includes(q)); }
const[field,dir]=state.sort.split('-');
fi.sort((a,b)=>{ let va,vb; switch(field){
case'date': va=a.date_added||'0'; vb=b.date_added||'0'; break;
case'days': va=a.days_since||0; vb=b.days_since||0; break;
case'name': va=(a.name||'').toLowerCase(); vb=(b.name||'').toLowerCase(); break;
case'salary': va=a.salary||0; vb=b.salary||0; break;
default: va=a.date_added||'0'; vb=b.date_added||'0';
} return va<vb?dir==='asc'?-1:1:va>vb?dir==='asc'?1:-1:0; });
state.filtered=fi; state.selected=new Set();
}
function render() {
const st=state.stats||{};
document.getElementById('kt').textContent=st.total||0; document.getElementById('km').textContent=st.strong_matches||0;
document.getElementById('kw').textContent=st.this_week||0; document.getElementById('ks').textContent=st.stale_14d||0;
document.getElementById('ku').textContent=st.closing_urgent||0; document.getElementById('kp').textContent=st.closing_past||0;
document.getElementById('kr').textContent=st.refreshed_at?new Date(st.refreshed_at).toLocaleString('en-AU',{timeZone:'Australia/Perth'}):'-';
const total=state.jobs.length||1, counts={}; state.jobs.forEach(j=>{counts[j.status]=(counts[j.status]||0)+1;});
document.getElementById('funnel').innerHTML=SORDER.map(s=>{const c=counts[s]||0; if(!c)return''; return'<div class="fi"><div class="fl"><span>'+s+'</span><span class="fc">'+c+'</span></div><div class="ftr"><div class="ff" style="width:'+(c/total*100).toFixed(0)+'%;background:'+(SCOLORS[s]||'#64748b')+'"></div></div></div>';}).join('');
const tb=document.getElementById('tb');
if(!state.filtered.length){ tb.innerHTML='<tr><td colspan="12" style="text-align:center;padding:48px 0;color:#64748b">No jobs match this filter</td></tr>'; return; }
tb.innerHTML=state.filtered.map(j=>{
const sel=state.selected.has(j.id)?'checked':'';
const sCol=SCOLORS[j.status]||'#64748b', sal=j.salary?'$'+Number(j.salary).toLocaleString():'—', age=(j.days_since||0)+'d';
const stale=(j.days_since||0)>14?'stale-14':(j.days_since||0)>7?'stale-7':'';
// Closing date urgency
let closeHtml = '—', clsCls = '';
if(j.closing_date){
const cd=new Date(j.closing_date);
const now=new Date();
const days=Math.round((cd-now)/86400000);
const ds=j.closing_date.split('-');
const fmt=ds[2]+'/'+ds[1];
closeHtml=fmt;
if(j.closing_status==='past'){ clsCls='cls-past'; }
else if(j.closing_status==='urgent'){ clsCls='cls-urgent'; }
else if(j.closing_status==='warning'){ clsCls='cls-warn'; }
}
const rowCls=[stale,clsCls].filter(Boolean).join(' ');
const name=j.url?'<a href="'+j.url+'" target="_blank">'+j.name+'</a>':j.name;
const mb=j.strong_match?'<span class="badge badge-sm">★</span>':'';
const ms=computeMatchScore(j), mc=matchColor(ms);
return '<tr class="'+rowCls+'" data-id="'+j.id+'"><td class="c-chk"><input type="checkbox" '+sel+' onchange="tog(\''+j.id+'\')"></td><td class="c-co">'+name+mb+'</td><td class="c-ro">'+(j.role||"—")+'</td><td class="c-st"><span class="stat-pill" style="background:'+sCol+'18;color:'+sCol+';border-color:'+sCol+'40" onclick="showMenu(event,\''+j.id+'\',\''+j.status+'\')">'+j.status+'</span></td><td class="c-lo">'+(j.location||"—")+'</td><td class="c-sr">'+(j.source||"—")+'</td><td class="c-sa">'+sal+'</td><td class="c-cls">'+closeHtml+'</td><td class="c-ag">'+age+'</td><td class="c-mt"><span class="match-pill" style="color:'+mc+';border-color:'+mc+'40" onclick="showMatchDetail(\''+j.id+'\',\''+ms+'\')">'+ms+'%</span></td><td class="c-cl">'+(j.cover_letter_url?'<a href="'+j.cover_letter_url+'" target="_blank" class="cl-link" title="View cover letter">📝</a><button class="cl-rg" onclick="regenerateCL(\''+j.id+'\')" title="Regenerate">🔄</button>':'<button class="cl-rg" onclick="regenerateCL(\''+j.id+'\')" title="Generate cover letter">+CL</button>')+'</td><td class="c-no"><button class="nb" onclick="editNote(\''+j.id+'\')">'+(j.notes?'✎':'+')+'</button></td></tr>';
}).join('');
document.getElementById('rc').textContent=state.filtered.length+' of '+state.jobs.length;
document.getElementById('bc').textContent=state.selected.size;
document.getElementById('bb').style.display=state.selected.size?'flex':'none';
}
function tog(id){ state.selected.has(id)?state.selected.delete(id):state.selected.add(id); render(); }
function selA(){ state.filtered.forEach(j=>state.selected.add(j.id)); render(); }
function desel(){ state.selected.clear(); render(); }
function setFilter(f){
state.filter=f;
if(state.view!=='table') setView('table');
// Update tab active states
document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('a',t.dataset.f===f));
// Update KPI card active states
document.getElementById('kf-strong').style.borderColor=f==='__strong'?'var(--cy)':'';
document.getElementById('kf-urgent').style.borderColor=f==='__urgent'?'var(--cy)':'#f59e0b';
document.getElementById('kf-past').style.borderColor=f==='__past'?'var(--cy)':'#64748b';
filter(); render();
}
function doSearch(){ state.search=document.getElementById('s').value; filter(); render(); }
function setSort(s){ state.sort=s; document.querySelectorAll('.sort-btn').forEach(b=>b.classList.toggle('a',b.dataset.s===s)); filter(); render(); }
function applyBulk(){ const v=document.getElementById('bs').value; bulk([...state.selected],{status:v}); }
function showMenu(ev,id,cur){
const m=document.getElementById('sm');
m.innerHTML=STATUSES.map(s=>'<div class="sm-item'+(s===cur?' sm-cur':'')+'" onclick="changeStat(\''+id+'\',\''+s+'\')">'+s+'</div>').join('');
m.style.display='block'; m.style.left=Math.min(ev.clientX,window.innerWidth-200)+'px'; m.style.top=ev.clientY+'px';
setTimeout(()=>document.addEventListener('click',function(){m.style.display='none';},{once:true}),10);
}
async function changeStat(id,s){ await upd(id,{status:s}); document.getElementById('sm').style.display='none'; }
function editNote(id){ const j=state.jobs.find(x=>x.id===id); const n=prompt('Edit notes / JD summary:',j&&j.notes?j.notes:''); if(n!==null) upd(id,{notes:n}); }
async function regenerateCL(id){
const j=state.jobs.find(x=>x.id===id);
const hasCL=j&&j.cover_letter_url;
const defaultFB=hasCL?'Make it stronger':'Generate a professional cover letter for this role';
const pText=hasCL?'How should the cover letter change? (e.g. "focus on CRM experience", "more professional tone"):':'What should the cover letter emphasise? (e.g. "highlight budget management", "reference government experience"):';
const fb=prompt(pText,defaultFB);
if(!fb)return;
const btn=event&&event.target||document.querySelector('[onclick*="'+id+'"]');
if(btn)btn.textContent='...';
const r=await f(API.clRegen(id),{method:'POST',body:JSON.stringify({feedback:fb})});
if(r&&r.ok){ notify('Cover letter '+(hasCL?'regenerated':'generated')+' ✅','ok'); load(); }
else{ notify('Generation failed: '+(r&&r.error||'unknown error'),'err'); if(btn)btn.textContent=hasCL?'🔄':'+CL'; }
}
document.addEventListener('DOMContentLoaded',function(){
// Theme init
if(localStorage.getItem('theme')==='light'){ document.documentElement.classList.add('light'); document.getElementById('themeBtn').textContent='🌙'; }
load(); setInterval(load,120000);
});
function toggleTheme(){
const h=document.documentElement; const b=document.getElementById('themeBtn');
h.classList.toggle('light');
const isLight=h.classList.contains('light');
b.textContent=isLight?'🌙':'☀️';
localStorage.setItem('theme',isLight?'light':'dark');
}
// ── View switching ──────────────────────────────────────────────────────────
function setView(v){
state.view=v;
document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('a',t.dataset.v===v));
document.getElementById('table-view').style.display=v==='table'?'block':'none';
document.getElementById('intel-view').style.display=v==='intel'?'block':'none';
if(v==='intel'&&state.marketIntel) renderIntel();
else if(v==='intel'&&!state.marketIntel) loadIntel();
}
async function loadIntel(){
const d=await f(API.marketIntel,{});
if(d){ state.marketIntel=d; renderIntel(); }
}
function showMatchDetail(id,score){
const j=state.jobs.find(x=>x.id===id);
if(!j) return;
let signals=[];
if(j.strong_match) signals.push('✓ Strong match flagged');
if(j.salary) signals.push('✓ Salary: $'+Number(j.salary).toLocaleString());
if(j.notes&&j.notes.length>50) signals.push('✓ Detailed JD summary');
else if(j.notes) signals.push('✓ Has JD notes');
if(j.cover_letter_url) signals.push('✓ Cover letter exists');
if(j.role) signals.push('✓ Role: '+j.role);
if(j.url) signals.push('✓ Job URL on file');
if(signals.length===0) signals=['No match signals yet — add notes or flag as strong match'];
document.getElementById('md-score').textContent=score+'%';
document.getElementById('md-signals').innerHTML=signals.map(s=>'<div class="md-sig">'+s+'</div>').join('');
document.getElementById('md-name').textContent=j.name+' — '+(j.role||'');
document.getElementById('md-overlay').style.display='flex';
}
function closeMatchDetail(){ document.getElementById('md-overlay').style.display='none'; }
// ── Market Intel rendering ───────────────────────────────────────────────────
function renderIntel(){
const mi=state.marketIntel;
if(!mi) return;
document.getElementById('mi-total').textContent=mi.total_tracked||0;
document.getElementById('mi-avg').textContent=mi.avg_salary?'$'+Number(mi.avg_salary).toLocaleString():'—';
document.getElementById('mi-median').textContent=mi.median_salary?'$'+Number(mi.median_salary).toLocaleString():'—';
document.getElementById('mi-range').textContent=(mi.min_salary&&mi.max_salary)?'$'+Number(mi.min_salary).toLocaleString()+' $'+Number(mi.max_salary).toLocaleString():'—';
document.getElementById('mi-with-sal').textContent=mi.with_salary+' of '+mi.total_tracked;
document.getElementById('mi-refreshed').textContent=mi.refreshed_at?new Date(mi.refreshed_at).toLocaleString('en-AU',{timeZone:'Australia/Perth'}):'';
// Salary distribution
const dist=mi.salary_distribution||[];
const maxCount=Math.max(...dist.map(d=>d.count),1);
document.getElementById('mi-dist').innerHTML=dist.map(d=>{
const pct=(d.count/maxCount*100).toFixed(0);
const fill=parseInt(d.label.replace(/[^0-9]/g,''))||0;
const hue=fill<80?200:fill<100?170:fill<120?140:fill<140?100:50;
return '<div class="mi-bar-row"><span class="mi-bar-label">'+d.label+'</span><div class="mi-bar-track"><div class="mi-bar-fill" style="width:'+pct+'%;background:hsl('+hue+',60%,50%)"></div></div><span class="mi-bar-count">'+d.count+'</span></div>';
}).join('');
// Location stats
const locs=mi.by_location||{};
const locEntries=Object.entries(locs).sort((a,b)=>b[1].count-a[1].count);
document.getElementById('mi-locs').innerHTML=locEntries.map(([loc,d])=>{
return '<div class="mi-loc-row"><span class="mi-loc-name">'+(loc||'Unknown')+'</span><span class="mi-loc-count">'+d.count+' jobs</span><span class="mi-loc-sal">$'+Number(d.avg).toLocaleString()+' avg</span></div>';
}).join('')||'<div style="color:var(--m);font-size:12px;padding:8px 0">No salary data by location</div>';
// Monthly trend
const months=mi.monthly_trend||{};
const monthEntries=Object.entries(months);
const maxMonth=Math.max(...monthEntries.map(m=>m[1]),1);
document.getElementById('mi-months').innerHTML=monthEntries.map(([m,c])=>{
const pct=(c/maxMonth*100).toFixed(0);
const label=m.slice(5)+'/'+m.slice(2,4);
return '<div class="mi-bar-row"><span class="mi-bar-label">'+label+'</span><div class="mi-bar-track"><div class="mi-bar-fill" style="width:'+pct+'%;background:hsl(190,70%,45%)"></div></div><span class="mi-bar-count">'+c+'</span></div>';
}).join('')||'<div style="color:var(--m);font-size:12px;padding:8px 0">No date data yet</div>';
// Source breakdown
const srcs=mi.by_source||{};
document.getElementById('mi-srcs').innerHTML=Object.entries(srcs).sort((a,b)=>b[1].count-a[1].count).map(([src,d])=>{
return '<div class="mi-loc-row"><span class="mi-loc-name">'+(src||'Unknown')+'</span><span class="mi-loc-count">'+d.count+' jobs</span><span class="mi-loc-sal">'+(d.avg_salary?'$'+Number(d.avg_salary).toLocaleString():'—')+'</span></div>';
}).join('');
}
</script>
<style>
:root{--bg:#0b1120;--srf:#141d30;--srf2:#1e2a45;--bdr:#1e3050;--txt:#e2e8f0;--m:#64748b;--cy:#22d3ee;--gr:#22c55e;--am:#f59e0b;--rd:#ef4444;--cz:#0b1120;--r-hover:rgba(34,211,238,0.02);--r-td:rgba(30,46,80,0.3)}
.light{--bg:#f8fafc;--srf:#ffffff;--srf2:#f1f5f9;--bdr:#e2e8f0;--txt:#0f172a;--cy:#0891b2;--cz:#ffffff;--r-hover:rgba(8,145,178,0.04);--r-td:rgba(15,23,42,0.06)}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--txt);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;transition:background .25s,color .25s}
.c{max-width:1440px;margin:0 auto;padding:16px}
.h{display:flex;justify-content:space-between;align-items:center;padding:8px 0 16px;border-bottom:1px solid var(--bdr);margin-bottom:20px;flex-wrap:wrap;gap:8px}
.h h1{font-size:18px;font-weight:700;color:var(--txt)}
.h .sub{font-size:12px;color:var(--m);margin-top:2px}
.h .rh{display:flex;align-items:center;gap:10px;font-size:12px;color:var(--m)}
.h .rh button{background:var(--srf2);color:var(--m);border:1px solid var(--srf2);padding:5px 12px;border-radius:6px;cursor:pointer;font-size:11px;font-weight:500}
.h .rh button:hover{border-color:var(--cy);color:var(--cy)}
.n{display:none;padding:10px 14px;border-radius:6px;margin-bottom:14px;font-size:13px}
.n.ok{background:rgba(34,197,94,0.12);color:var(--gr);border:1px solid rgba(34,197,94,0.2)}
.n.err{background:rgba(239,68,68,0.12);color:var(--rd);border:1px solid rgba(239,68,68,0.2)}
.k{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:18px}
.kc{background:var(--srf);border-radius:8px;padding:14px;border:1px solid var(--bdr)}
.kc .kv{font-size:26px;font-weight:700;letter-spacing:-0.02em;line-height:1.1}
.kc .kl{font-size:11px;color:var(--m);margin-top:3px;font-weight:500}
.kc.cy .kv{color:var(--cy)} .kc.gr .kv{color:var(--gr)} .kc.am .kv{color:var(--am)} .kc.rd .kv{color:var(--rd)}
.fu{background:var(--srf);border-radius:8px;padding:16px;margin-bottom:18px;border:1px solid var(--bdr)}
.fi{margin-bottom:5px} .fl{display:flex;justify-content:space-between;font-size:12px;margin-bottom:3px} .fc{color:var(--m)}
.ftr{height:5px;background:var(--srf2);border-radius:3px;overflow:hidden} .ff{height:100%;border-radius:3px}
.bulk{display:none;align-items:center;gap:10px;padding:8px 14px;background:rgba(34,211,238,0.06);border:1px solid rgba(34,211,238,0.15);border-radius:6px;margin-bottom:12px;font-size:12px}
.bulk select{background:var(--srf2);color:var(--txt);border:1px solid var(--bdr);padding:4px 8px;border-radius:4px;font-size:12px}
.bulk .b-btn{background:var(--cy);color:var(--cz);border:none;padding:5px 12px;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600}
.bulk .b-btn2{background:var(--srf2);color:var(--m);border:1px solid var(--bdr);padding:5px 12px;border-radius:4px;cursor:pointer;font-size:11px}
.ct{display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap;align-items:center}
.tabs{display:flex;gap:3px;flex-wrap:wrap}
.tab{padding:5px 14px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:500;background:var(--srf);color:var(--m);border:1px solid transparent}
.tab:hover{color:var(--txt)} .tab.a{background:var(--cy);color:var(--cz)}
.sbox{background:var(--srf);border:1px solid var(--bdr);padding:5px 10px;border-radius:6px;color:var(--txt);font-size:12px;width:180px}
.sbox:focus{outline:none;border-color:var(--cy)}
.sg{display:flex;gap:2px;margin-left:auto}
.sort-btn{padding:4px 10px;border-radius:4px;cursor:pointer;font-size:11px;color:var(--m);border:1px solid transparent}
.sort-btn:hover{color:var(--txt)} .sort-btn.a{border-color:var(--cy);color:var(--cy)}
.tw{background:var(--srf);border-radius:8px;overflow:hidden;border:1px solid var(--bdr)}
.th{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;border-bottom:1px solid var(--bdr)}
.th h2{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--m)}
.th .rc{color:var(--m);font-size:11px}
.ts{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:12px}
th{text-align:left;padding:8px 10px;font-weight:600;color:var(--m);font-size:10px;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--bdr);position:sticky;top:0;background:var(--srf);white-space:nowrap}
td{padding:6px 10px;border-bottom:1px solid var(--r-td);vertical-align:middle}
tr:hover td{background:var(--r-hover)}
.stale-14 td{opacity:.65;color:var(--rd)} .stale-7 td{opacity:.8;color:var(--am)}
/* Closing date urgency */
.cls-past td{opacity:0.4}
.cls-urgent .c-cls{color:var(--rd);font-weight:700}
.cls-warn .c-cls{color:var(--am)}
.c-chk{width:28px} .c-co{min-width:160px} .c-co a{color:var(--cy);text-decoration:none;font-weight:500}
.c-co a:hover{text-decoration:underline} .c-ro{min-width:170px;color:var(--m)} .c-lo{min-width:100px;color:var(--m)}
.c-sr{min-width:65px;color:var(--m)} .c-sa{text-align:right;white-space:nowrap} .c-cls{text-align:right;white-space:nowrap;font-size:11px}
.c-ag{text-align:right;white-space:nowrap;color:var(--m)}
.c-no{width:30px;text-align:center}
.c-cl{width:55px;text-align:center;white-space:nowrap}
.cl-link{text-decoration:none;font-size:15px;cursor:pointer;margin-right:4px}
.cl-link:hover{filter:brightness(1.3)}
.cl-rg{background:none;border:none;color:var(--m);cursor:pointer;font-size:11px;padding:2px 5px;border-radius:4px}
.cl-rg:hover{background:var(--srf2);color:var(--cy)}
.stat-pill{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:500;cursor:pointer;border:1px solid transparent}
.stat-pill:hover{filter:brightness(1.2)}
.nb{background:none;border:none;color:var(--m);cursor:pointer;font-size:13px;padding:2px 4px;border-radius:3px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center}
.nb:hover{background:var(--srf2);color:var(--txt)}
.badge{display:inline-block;font-size:9px;padding:1px 5px;border-radius:3px;font-weight:600;margin-left:4px}
.badge-sm{background:rgba(34,211,238,0.12);color:var(--cy)}
.sm{display:none;position:fixed;z-index:100;background:var(--srf);border:1px solid var(--bdr);border-radius:6px;padding:3px;box-shadow:0 8px 16px rgba(0,0,0,.3);min-width:140px}
.sm-item{padding:6px 10px;border-radius:4px;cursor:pointer;font-size:12px}
.sm-item:hover{background:var(--srf2)} .sm-cur{background:var(--cy);color:var(--cz);font-weight:600}
.ft{text-align:center;padding:20px 0 12px;color:var(--m);font-size:10px} .ft a{color:var(--cy);text-decoration:none}
.match-pill{display:inline-block;padding:1px 6px;border-radius:4px;font-size:11px;font-weight:600;border:1px solid;cursor:pointer}
.match-pill:hover{filter:brightness(1.3)}
.c-mt{width:50px;text-align:center}
/* Market Intel panel */
.intel-panel{display:none;background:var(--srf);border-radius:8px;border:1px solid var(--bdr);padding:16px;margin-bottom:18px}
.intel-panel h2{font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--cy);margin-bottom:14px}
.intel-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:18px}
.intel-card{background:var(--srf2);border-radius:6px;padding:10px;text-align:center}
.intel-card .val{font-size:20px;font-weight:700;color:var(--txt);line-height:1.2}
.intel-card .lbl{font-size:10px;color:var(--m);margin-top:2px}
.intel-grid-2{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px}
@media(max-width:700px){.intel-grid-2{grid-template-columns:1fr}}
.intel-section{margin-bottom:16px}
.intel-section h3{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--m);margin-bottom:8px}
.mi-bar-row{display:flex;align-items:center;gap:8px;margin-bottom:4px;font-size:12px}
.mi-bar-label{width:95px;text-align:right;color:var(--m);flex-shrink:0;font-size:11px}
.mi-bar-track{flex:1;height:18px;background:var(--srf2);border-radius:3px;overflow:hidden}
.mi-bar-fill{height:100%;border-radius:3px;transition:width .5s}
.mi-bar-count{width:28px;text-align:right;color:var(--txt);font-weight:500;font-size:11px}
.mi-loc-row{display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid var(--r-td);font-size:12px}
.mi-loc-row:last-child{border:none}
.mi-loc-name{flex:1;color:var(--txt)}
.mi-loc-count{color:var(--m);font-size:11px;white-space:nowrap}
.mi-loc-sal{color:var(--cy);font-size:11px;font-weight:500;text-align:right;min-width:80px}
/* Match detail overlay */
.md-overlay{display:none;position:fixed;inset:0;z-index:200;background:rgba(0,0,0,.6);align-items:center;justify-content:center}
.md-card{background:var(--srf);border:1px solid var(--bdr);border-radius:10px;padding:20px;max-width:400px;width:90%;max-height:80vh;overflow-y:auto}
.md-card h3{font-size:14px;font-weight:600;margin-bottom:4px}
.md-card .sub{font-size:12px;color:var(--m);margin-bottom:12px}
.md-score-big{font-size:40px;font-weight:800;text-align:center;padding:10px 0;letter-spacing:-.03em}
.md-signals{font-size:12px;line-height:1.8}
.md-sig{padding:3px 0;color:var(--txt)}
.md-close{display:block;width:100%;padding:8px;margin-top:14px;background:var(--srf2);border:1px solid var(--bdr);border-radius:6px;color:var(--m);font-size:12px;cursor:pointer;text-align:center}
.md-close:hover{color:var(--txt);border-color:var(--cy)}
@media(max-width:768px){.c{padding:10px}.k{grid-template-columns:repeat(2,1fr)}.sbox{width:140px}.c-lo,.c-sr,.c-sa{display:none}}
</style>
</head>
<body><div class="c">
<div class="h"><div><h1>Job Tracker</h1><div class="sub">Anthony Martin · Perth, WA</div></div><div class="rh"><span id="kr"></span><button onclick="toggleTheme()" id="themeBtn" title="Toggle light/dark mode">☀️</button><button onclick="refresh()">↻ Refresh</button></div></div>
<div id="n" class="n"></div>
<div class="k"><div class="kc cy" onclick="setFilter('all')" style="cursor:pointer"><div class="kv" id="kt"></div><div class="kl">Total Jobs</div></div><div class="kc gr" id="kf-strong" onclick="setFilter('__strong')" style="cursor:pointer"><div class="kv" id="km"></div><div class="kl">Strong Matches</div></div><div class="kc am"><div class="kv" id="kw"></div><div class="kl">This Week</div></div><div class="kc rd"><div class="kv" id="ks"></div><div class="kl">Stale (14d+)</div></div><div class="kc" id="kf-urgent" onclick="setFilter('__urgent')" style="border-color:#f59e0b;cursor:pointer"><div class="kv" id="ku"></div><div class="kl" style="color:var(--am)">Expiring Soon</div></div><div class="kc" id="kf-past" onclick="setFilter('__past')" style="border-color:#64748b;cursor:pointer"><div class="kv" id="kp"></div><div class="kl" style="color:var(--m)">Past Closing</div></div></div>
<div class="fu" id="funnel"></div>
<div class="bulk" id="bb"><span><strong id="bc">0</strong> selected</span><span>Status:</span><select id="bs"></select><button class="b-btn" onclick="applyBulk()">Apply</button><button class="b-btn2" onclick="desel()">Clear</button></div>
<div class="ct"><div class="tabs" id="tabs"><span class="tab a" data-v="table" onclick="setView('table')">📋 All Jobs</span><span class="tab" data-v="intel" onclick="setView('intel')">📊 Market Intel</span><span class="tab" data-f="all" onclick="setFilter('all')">All</span><span class="tab" data-f="Backlog" onclick="setFilter('Backlog')">Backlog</span><span class="tab" data-f="Applied" onclick="setFilter('Applied')">Applied</span><span class="tab" data-f="Interview" onclick="setFilter('Interview')">Interviews</span><span class="tab" data-f="Rejected" onclick="setFilter('Rejected')">Rejected</span></div>
<input class="sbox" id="s" placeholder="Search..." oninput="doSearch()">
<div class="sg"><span class="sort-btn a" data-s="date-desc" onclick="setSort('date-desc')">Newest</span><span class="sort-btn" data-s="days-desc" onclick="setSort('days-desc')">Stalest</span><span class="sort-btn" data-s="salary-desc" onclick="setSort('salary-desc')">Salary</span></div></div>
<div id="table-view"><div class="tw"><div class="th"><h2>Opportunities</h2><span class="rc" id="rc"></span></div>
<div class="ts"><table><thead><tr><th><input type="checkbox" onchange="if(this.checked)selA();else desel();"></th><th>Company</th><th>Role</th><th>Status</th><th>Location</th><th>Source</th><th style="text-align:right">Salary</th><th style="text-align:right">Closing</th><th style="text-align:right">Age</th><th style="text-align:center">Match</th><th>CL</th><th></th></tr></thead>
<tbody id="tb"><tr><td colspan="12" style="text-align:center;padding:48px 0;color:var(--m)">Loading...</td></tr></tbody></table></div></div></div>
<!-- Market Intel Panel -->
<div id="intel-view" class="intel-panel">
<h2>📊 Market Intel</h2>
<div class="intel-grid">
<div class="intel-card"><div class="val" id="mi-total"></div><div class="lbl">Total Tracked</div></div>
<div class="intel-card"><div class="val" id="mi-avg"></div><div class="lbl">Avg Salary</div></div>
<div class="intel-card"><div class="val" id="mi-median"></div><div class="lbl">Median Salary</div></div>
<div class="intel-card"><div class="val" id="mi-range"></div><div class="lbl">Salary Range</div></div>
<div class="intel-card"><div class="val" id="mi-with-sal"></div><div class="lbl">With Salary Data</div></div>
</div>
<div class="intel-grid-2">
<div class="intel-section">
<h3>Salary Distribution</h3>
<div id="mi-dist"></div>
</div>
<div class="intel-section">
<h3>By Location</h3>
<div id="mi-locs"></div>
</div>
</div>
<div class="intel-grid-2">
<div class="intel-section">
<h3>Monthly Trend (jobs added)</h3>
<div id="mi-months"></div>
</div>
<div class="intel-section">
<h3>By Source</h3>
<div id="mi-srcs"></div>
</div>
</div>
<div style="text-align:center;color:var(--m);font-size:10px;padding-top:8px">Updated from Notion · <span id="mi-refreshed"></span></div>
</div>
<div class="ft">The Rhino in the Green Jacket · <a href="/api/jobs">API</a> · <a href="/ops">Ops</a></div></div>
<div id="sm" class="sm"></div>
<!-- Match Detail Overlay -->
<div id="md-overlay" class="md-overlay" onclick="if(event.target===this)closeMatchDetail()">
<div class="md-card">
<h3 id="md-name"></h3>
<div class="sub">Match Analysis</div>
<div class="md-score-big" style="color:var(--gr)" id="md-score"></div>
<div class="md-signals" id="md-signals"></div>
<button class="md-close" onclick="closeMatchDetail()">Close</button>
</div>
</div>
<script>
// Populate bulk status dropdown after STATUSES is defined
document.getElementById('bs').innerHTML=STATUSES.map(s=>'<option value="'+s+'">'+s+'</option>').join('');
</script>
</body></html>