491 lines
24 KiB
HTML
491 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1">
|
|
<title>Evening Stand-Down — Rhino Terminal</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
:root{
|
|
--bg:#0d0f14;--surf:#141820;--card:#181c26;
|
|
--border:#1e2332;--amber:#ffb000;--amber-bright:#ffd866;
|
|
--amber-dim:#665500;--amber-faint:#2a2200;
|
|
--green:#4caf7d;--red:#ef5350;--cyan:#7ae9ff;
|
|
--muted:#3a3e4a;
|
|
--radius:0;--font:'JetBrains Mono','Fira Code','Cascadia Code',monospace
|
|
}
|
|
body{
|
|
font-family:var(--font);background:var(--bg);color:var(--amber);
|
|
min-height:100vh;padding:16px;line-height:1.7;
|
|
-webkit-font-smoothing:antialiased;
|
|
font-size:14px
|
|
}
|
|
.container{max-width:600px;margin:0 auto}
|
|
|
|
/* ── CRT Glow ───────────────────────────────────────── */
|
|
body::after{
|
|
content:'';position:fixed;top:0;left:0;width:100%;height:100%;
|
|
pointer-events:none;
|
|
background:radial-gradient(ellipse at center,rgba(255,176,0,0.015) 0%,transparent 70%);
|
|
z-index:999
|
|
}
|
|
|
|
/* ── Header ─────────────────────────────────────────── */
|
|
.term-bar{
|
|
border:1px solid var(--amber-faint);border-radius:4px 4px 0 0;
|
|
background:var(--surf);overflow:hidden
|
|
}
|
|
.term-title{
|
|
display:flex;align-items:center;justify-content:space-between;
|
|
padding:6px 12px;background:var(--card);border-bottom:1px solid var(--amber-faint);
|
|
font-size:11px;color:var(--amber-dim);user-select:none
|
|
}
|
|
.term-title .dot{
|
|
display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px
|
|
}
|
|
.term-title .dots{display:flex;gap:4px}
|
|
.term-title .dots .dot{width:10px;height:10px;margin:0}
|
|
.dot.r{background:#ff5f56}.dot.y{background:#ffbd2e}.dot.g{background:#27c93f}
|
|
.term-title .name{font-size:10px;text-transform:uppercase;letter-spacing:0.8px}
|
|
.term-body{padding:18px 16px 14px;min-height:200px}
|
|
|
|
.prompt-line{
|
|
display:flex;align-items:center;gap:2px;margin-bottom:2px;
|
|
font-size:13px;color:var(--amber-dim)
|
|
}
|
|
.prompt-line .ps1{color:var(--amber-bright);font-weight:500}
|
|
.prompt-line .ps2{color:var(--muted)}
|
|
.blink-cursor::after{
|
|
content:'█';animation:blink 1s step-end infinite;
|
|
color:var(--amber);font-size:12px;margin-left:1px
|
|
}
|
|
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
|
|
@keyframes rippleAnim{from{transform:scale(0);opacity:1}to{transform:scale(2.5);opacity:0}}
|
|
|
|
.header-content{padding:6px 0 12px}
|
|
.header-content .greeting{font-size:16px;font-weight:700;color:var(--amber-bright)}
|
|
.header-content .date{font-size:12px;color:var(--amber-dim);margin-top:2px}
|
|
.header-content .lin{font-size:11px;color:var(--muted);margin-top:2px}
|
|
.header-content .lin sp{color:var(--amber-dim)}
|
|
|
|
/* ── Terminal Section Cards ────────────────────────── */
|
|
.sect{margin-bottom:6px}
|
|
.sect-head{
|
|
display:flex;align-items:center;gap:8px;
|
|
padding:10px 12px;cursor:pointer;user-select:none;
|
|
border:1px solid var(--border);border-radius:0;
|
|
background:var(--surf);
|
|
transition:border-color 0.25s,background 0.2s;
|
|
-webkit-tap-highlight-color:transparent;
|
|
font-family:var(--font);font-size:13px;color:var(--amber-dim);position:relative;overflow:hidden
|
|
}
|
|
.sect-head:hover{border-color:var(--amber-faint);background:var(--card)}
|
|
.sect-head .ico{font-size:16px;width:22px;text-align:center;flex-shrink:0}
|
|
.sect-head .lb{flex:1;color:var(--amber);font-weight:500;letter-spacing:-0.3px}
|
|
.sect-head .ar{font-size:10px;color:var(--amber-dim);transition:transform 0.4s cubic-bezier(0.34,1.56,0.64,1)}
|
|
.sect.open .sect-head .ar{transform:rotate(180deg)}
|
|
.sect.open .sect-head{background:var(--card);border-color:var(--amber-faint)}
|
|
|
|
.sect-body{
|
|
max-height:0;overflow:hidden;
|
|
transition:max-height 0.5s cubic-bezier(0.22,1,0.36,1);
|
|
border-left:1px solid var(--amber-faint);border-right:1px solid var(--amber-faint)
|
|
}
|
|
.sect.open .sect-body{max-height:900px}
|
|
.sect-inner{padding:10px 14px 14px;border-top:1px solid var(--amber-faint)}
|
|
|
|
/* ── Terminal-style Content ─────────────────────────── */
|
|
.kpi-grid{
|
|
display:grid;grid-template-columns:repeat(3,1fr);gap:1px;
|
|
background:var(--border);border:1px solid var(--border)
|
|
}
|
|
.kpi-grid>div{
|
|
background:var(--surf);padding:12px 6px;text-align:center
|
|
}
|
|
.kpi-grid .v{font-size:28px;font-weight:700;line-height:1;margin-bottom:2px}
|
|
.kpi-grid .l{font-size:9px;text-transform:uppercase;letter-spacing:1px;color:var(--amber-dim)}
|
|
|
|
.arrow-join{position:relative;padding:2px 0 2px 18px}
|
|
.arrow-join::before{
|
|
content:'▸';position:absolute;left:0;color:var(--amber-dim);font-size:11px
|
|
}
|
|
|
|
.role-line{
|
|
padding:8px 0 8px 18px;position:relative;
|
|
border-bottom:1px solid var(--border)
|
|
}
|
|
.role-line:last-child{border:0}
|
|
.role-line::before{
|
|
content:'▶';position:absolute;left:0;top:10px;font-size:8px;color:var(--amber-dim)
|
|
}
|
|
.role-line .co{font-size:13px;font-weight:500;color:var(--amber-bright)}
|
|
.role-line .ti{font-size:11px;color:var(--amber-dim);margin-top:1px}
|
|
.role-line .tg{
|
|
display:flex;gap:6px;margin-top:4px;font-size:10px
|
|
}
|
|
.role-line .tg sp{
|
|
padding:1px 6px;border:1px solid var(--amber-faint);
|
|
color:var(--amber-dim);border-radius:2px
|
|
}
|
|
.role-line .tg .sal{border-color:rgba(76,175,125,0.3);color:var(--green)}
|
|
.role-line .tg .src{border-color:var(--amber-faint);color:var(--amber-dim)}
|
|
|
|
.wdg{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:6px}
|
|
.wdg sp{
|
|
font-size:10px;padding:3px 8px;border:1px solid var(--border)
|
|
}
|
|
.wdg .ok{border-color:rgba(76,175,125,0.3);color:var(--green)}
|
|
.wdg .err{border-color:rgba(239,83,80,0.3);color:var(--red)}
|
|
.wdg .neu{border-color:var(--amber-faint);color:var(--amber-dim)}
|
|
|
|
.w-row{
|
|
display:flex;justify-content:space-between;align-items:center;
|
|
padding:4px 0
|
|
}
|
|
.w-row .lft{display:flex;align-items:center;gap:10px}
|
|
.w-row .lft .t{font-size:30px;font-weight:700;line-height:1}
|
|
.w-row .lft .c{font-size:11px;color:var(--amber-dim)}
|
|
.w-row .rgt{text-align:right;font-size:11px;color:var(--amber-dim)}
|
|
.w-row .rgt .b{font-weight:500;color:var(--amber-dbright);font-size:13px}
|
|
|
|
.mosq-box{text-align:center;padding:6px 0 2px}
|
|
.mosq-box .b{font-size:36px;display:inline-block;animation:buzz 1.5s ease-in-out infinite}
|
|
@keyframes buzz{
|
|
0%,100%{transform:translate(0,0)rotate(0)}
|
|
20%{transform:translate(1px,-1px)rotate(2deg)}
|
|
40%{transform:translate(-2px,1px)rotate(-2deg)}
|
|
60%{transform:translate(1px,1px)rotate(1deg)}
|
|
80%{transform:translate(-1px,-1px)rotate(-1deg)}
|
|
}
|
|
.mosq-box .m{margin-top:6px;font-size:12px;color:var(--amber-dim);line-height:1.6;font-style:italic}
|
|
.mosq-box .z{font-size:9px;color:var(--muted);margin-top:4px}
|
|
|
|
.rhn-box{text-align:center;padding:4px 0}
|
|
.rhn-box .i{font-size:30px;margin-bottom:4px}
|
|
.rhn-box .m{font-size:13px;line-height:1.6;color:var(--amber);font-style:italic}
|
|
.rhn-box .s{font-size:9px;color:var(--muted);margin-top:6px}
|
|
|
|
.empty{color:var(--muted);font-size:12px;text-align:center;padding:12px 0}
|
|
|
|
.dash-link{
|
|
display:block;text-align:center;
|
|
padding:8px;margin-top:8px;
|
|
border:1px solid var(--amber-faint);border-radius:0;
|
|
color:var(--amber);text-decoration:none;font-size:11px;
|
|
transition:background 0.2s
|
|
}
|
|
.dash-link:hover{background:var(--amber-faint)}
|
|
|
|
/* ── Footer ─────────────────────────────────────────── */
|
|
.term-footer{
|
|
border:1px solid var(--amber-faint);border-top:0;
|
|
padding:8px 12px;background:var(--surf);
|
|
font-size:10px;color:var(--amber-dim);text-align:center;border-radius:0 0 4px 4px
|
|
}
|
|
|
|
/* ── Animations ─────────────────────────────────────── */
|
|
@keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|
.anim{animation:fadeUp 0.4s ease-out forwards;opacity:0}
|
|
.anim.d1{animation-delay:0.05s}.anim.d2{animation-delay:0.1s}
|
|
.anim.d3{animation-delay:0.15s}.anim.d4{animation-delay:0.2s}
|
|
.anim.d5{animation-delay:0.25s}.anim.d6{animation-delay:0.3s}
|
|
.anim.d7{animation-delay:0.35s}.anim.d8{animation-delay:0.4s}
|
|
|
|
@media(prefers-reduced-motion:reduce){
|
|
.anim{animation-duration:0.15s}
|
|
.mosq-box .b{animation:none}
|
|
@keyframes blink{0%,100%{opacity:1}}
|
|
}
|
|
|
|
@media(max-width:480px){
|
|
body{padding:10px;font-size:13px}
|
|
.term-body{padding:14px 12px 10px}
|
|
.kpi-grid .v{font-size:22px}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container anim">
|
|
<!-- Terminal Window -->
|
|
<div class="term-bar">
|
|
<div class="term-title">
|
|
<span class="name">🌆 rhino-terminal — evening-stand-down</span>
|
|
<div class="dots">
|
|
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
|
|
</div>
|
|
</div>
|
|
<div class="term-body">
|
|
<div class="prompt-line"><span class="ps1">rhino</span><span class="ps2">@</span><span class="ps1">jacket</span><span class="ps2">:~$ </span><span id="greetingCmd">./briefing --mode=stand-down</span><span class="blink-cursor"></span></div>
|
|
|
|
<div class="header-content anim d1">
|
|
<div class="greeting" id="greetingLine">Evening Stand-Down</div>
|
|
<div class="date" id="dateLine">Loading…</div>
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:6px">
|
|
<div class="lin">─── <sp>tap each line</sp> to read the briefing ───</div>
|
|
<div id="progressLabel" style="display:none;font-size:10px;color:var(--amber-dim);background:var(--card);padding:2px 8px;border:1px solid var(--amber-faint)"><span id="progressCount">0/7</span> <span id="progressBar">sections</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section Cards -->
|
|
<div id="cards" class="anim d2"></div>
|
|
|
|
<!-- Footer -->
|
|
<div class="term-footer">
|
|
rhino@jacket:~$ <span id="footerCmd">last brief</span><span class="blink-cursor"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const QUOTES=[
|
|
"You're not looking for a job — you're curating your next chapter.",
|
|
"Every application is a data point. The more you send, the tighter your signal.",
|
|
"The job you want is also looking for you. Keep showing up.",
|
|
"Consistency beats intensity. You showed up today. That's a win.",
|
|
"Your resume is a story. Every role you apply to is a new audience.",
|
|
"The market is noisy. Your signal is the green jacket. Stand out.",
|
|
"Rejection is redirection. Every no is one closer to the right yes.",
|
|
"You've built brands. Now build your own narrative.",
|
|
"The rhino doesn't rush. The rhino is patient. The rhino gets results.",
|
|
"Three applications this week is three conversations you didn't have last week.",
|
|
];
|
|
function gQ(d){return QUOTES[d%QUOTES.length]}
|
|
|
|
const MOSQ=[
|
|
"The mosquito was seen loitering near the database server. The jacket is monitoring.",
|
|
"🦟 Radar contact lost over the cron scheduler. Presumed plotting something.",
|
|
"The mosquito has been spotted attempting to learn Python. Threat level: amused.",
|
|
"Mosquito activity detected around port 9099. The jacket installed a tiny bug zapper.",
|
|
"The mosquito issued a manifesto. Three words: 'buzz buzz buzz'. We're watching.",
|
|
"No mosquito sightings today. Either planning something big, or it took a day off.",
|
|
"The mosquito tried to submit a PR. Denied for lack of credentials.",
|
|
"SIGINT suggests the mosquito is recruiting. The jacket is building countermeasures.",
|
|
"The mosquito sent a cease-and-desist. The jacket laughed. It never stops.",
|
|
"Mosquito attempted to file a bug report. Closed: 'working as intended'.",
|
|
"Agent Mosquito last seen heading toward the Tailscale tunnel. Perimeter secure.",
|
|
"The mosquito has been quiet. Too quiet. The jacket is not fooled.",
|
|
];
|
|
function gM(d){return MOSQ[d%MOSQ.length]}
|
|
|
|
const RHINO=[
|
|
"The jacket and I reviewed your week. You're making progress, even when it doesn't feel like it. Keep going.",
|
|
"I've consulted the pipe. The pipe says: 'You've got this.' The pipe is rarely wrong.",
|
|
"A gentle reminder from the Monocled Menace: the right role won't find you unless you're putting yourself out there. You are.",
|
|
"Three things the jacket knows: (1) you're capable, (2) you're consistent, (3) you look good in green.",
|
|
"The rhino does not panic. The rhino adapts. Today you adapted. That's growth.",
|
|
"One does not simply apply to jobs — one curates one's destiny. You curated today. The jacket approves.",
|
|
"You are not behind. You are exactly on time for where you need to be.",
|
|
"I was once mistaken for a very distinguished armchair. I sat with it. Now I'm telling you: sit with patience. It pays off.",
|
|
"The green jacket has seen many campaigns. This one's not over yet.",
|
|
"You applied to roles that didn't exist six months ago. You're ahead and don't know it.",
|
|
];
|
|
function gR(d){return RHINO[d%RHINO.length]}
|
|
|
|
function wi(c){c=(c||'').toLowerCase()
|
|
if(c.includes('sun')||c.includes('clear'))return'☀️';if(c.includes('rain')||c.includes('drizzle')||c.includes('shower'))return'🌧️'
|
|
if(c.includes('cloud')||c.includes('overcast'))return'☁️';if(c.includes('fog')||c.includes('mist'))return'🌫️'
|
|
if(c.includes('thunder')||c.includes('storm'))return'⛈️';if(c.includes('wind'))return'💨';return'🌤️'}
|
|
function es(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
|
|
|
/* ── Typewriter Effect ───────────────────────────────── */
|
|
function typewrite(el,text,speed=35,cb){
|
|
let i=0;el.textContent='';const iv=setInterval(()=>{
|
|
el.textContent+=text[i++];if(i>=text.length){clearInterval(iv);if(cb)cb()}
|
|
},speed)
|
|
}
|
|
|
|
function typewriteFull(text,speed,cb){
|
|
const el=document.getElementById('greetingCmd');el.textContent='';
|
|
typewrite(el,text,speed,cb)
|
|
}
|
|
|
|
/* ── Animated Counters ────────────────────────────────── */
|
|
function ani(el,t){
|
|
const d=800,s=performance.now();
|
|
function f(n){
|
|
const p=Math.min((n-s)/d,1),e=1-Math.pow(1-p,3);
|
|
el.textContent=Math.floor(0+(t-0)*e);
|
|
if(p<1)requestAnimationFrame(f)
|
|
}
|
|
requestAnimationFrame(f)
|
|
}
|
|
|
|
/* ── Section Counter ──────────────────────────────────── */
|
|
let readCount=0;const totalSections=7;
|
|
function updateProgress(){
|
|
readCount++;
|
|
const el=document.getElementById('progressCount');
|
|
if(el)el.textContent=`${readCount}/${totalSections}`;
|
|
// Check if all read
|
|
if(readCount===totalSections){
|
|
setTimeout(()=>{
|
|
const bar=document.getElementById('progressBar');
|
|
if(bar)bar.textContent='✓ all sections read';
|
|
},400)
|
|
}
|
|
}
|
|
|
|
/* ── Tap Ripple ────────────────────────────────────────── */
|
|
function ripple(e){
|
|
const el=e.currentTarget;
|
|
const r=document.createElement('span');
|
|
r.style.cssText=`position:absolute;pointer-events:none;border-radius:50%;
|
|
background:rgba(255,176,0,0.08);transform:scale(0);animation:rippleAnim 0.5s ease-out forwards;
|
|
width:${Math.max(el.offsetWidth,el.offsetHeight)}px;
|
|
height:${Math.max(el.offsetWidth,el.offsetHeight)}px;
|
|
left:50%;top:50%;margin-left:-${Math.max(el.offsetWidth,el.offsetHeight)/2}px;
|
|
margin-top:-${Math.max(el.offsetWidth,el.offsetHeight)/2}px;z-index:0`;
|
|
el.appendChild(r);
|
|
// Style the label to be above ripple
|
|
setTimeout(()=>r.remove(),500)
|
|
}
|
|
|
|
/* ── Build Section Card ────────────────────────────────── */
|
|
function mkSect(icon,title,contentFn,delay,dataObj){
|
|
const div=document.createElement('div');div.className=`sect anim d${delay}`;
|
|
div.innerHTML=`<div class="sect-head"><span class="ico">${icon}</span>
|
|
<span class="lb">${es(title)}</span><span class="ar">▾</span></div>
|
|
<div class="sect-body"><div class="sect-inner"></div></div>`;
|
|
const h=div.querySelector('.sect-head'),b=div.querySelector('.sect-inner');let opened=false;
|
|
h.addEventListener('click',(e)=>{
|
|
const wasOpen=div.classList.contains('open');
|
|
if(wasOpen){div.classList.remove('open');return}
|
|
// Close others
|
|
document.querySelectorAll('.sect.open').forEach(c=>c.classList.remove('open'));
|
|
div.classList.add('open');
|
|
ripple(e);
|
|
if(!opened){opened=true;contentFn(dataObj,b,div)}
|
|
updateProgress();
|
|
});
|
|
return div
|
|
}
|
|
|
|
/* ── Content Renderers ────────────────────────────────── */
|
|
function rRecap(d,b,section){
|
|
const j=d.jobs||{};const s=j.status_breakdown||{};const t=Object.values(s).reduce((a,v)=>a+v,0)||0;
|
|
b.innerHTML=`
|
|
<div class="kpi-grid">
|
|
<div><div class="v" style="color:var(--amber-bright)"><sp1 class="c-n">0</sp1></div><div class="l">New Today</div></div>
|
|
<div><div class="v" style="color:var(--green)"><sp2 class="c-s">0</sp2></div><div class="l">Strong</div></div>
|
|
<div><div class="v" style="color:var(--red)"><sp3 class="c-u">0</sp3></div><div class="l">Urgent</div></div>
|
|
</div>
|
|
<div style="text-align:center;font-size:11px;color:var(--amber-dim);margin-top:6px">${t} roles tracked · ${j.this_week||0} this week</div>`;
|
|
setTimeout(()=>{ani(b.querySelector('.c-n'),j.today_added||0);ani(b.querySelector('.c-s'),j.strong_matches||0);ani(b.querySelector('.c-u'),j.urgent_closing||0)},150)
|
|
}
|
|
|
|
function rRoles(d,b){
|
|
const r=(d.jobs&&d.jobs.today_roles)||[];
|
|
if(!r.length){b.innerHTML=`<div class="empty">⏳ No new matches today</div>`;return}
|
|
const sal=d.jobs.salary_range;
|
|
let h='';if(sal)h+=`<div class="arrow-join" style="font-size:11px;color:var(--amber-dim);margin-bottom:4px">salary range: $${(sal.min/1000).toFixed(0)}k – $${(sal.max/1000).toFixed(0)}k</div>`;
|
|
r.forEach(o=>{
|
|
const nm=es(o.name);
|
|
const link=o.url?`<a href="${es(o.url)}" target="_blank" rel="noopener" style="color:var(--amber-bright);text-decoration:none;border-bottom:1px dashed var(--amber-faint)">${nm}</a>`:nm;
|
|
h+=`<div class="role-line"><div class="co">${link}</div><div class="ti">${es(o.role)}</div><div class="tg">${o.salary?`<sp class="sal">$${(o.salary/1000).toFixed(0)}k</sp>`:''}<sp class="src">${es(o.source)}</sp></div></div>`
|
|
});
|
|
b.innerHTML=h
|
|
}
|
|
|
|
function rClosing(d,b){
|
|
const list=(d.jobs&&d.jobs.closing_soon)||[];
|
|
if(!list.length){b.innerHTML=`<div class="empty">✓ No upcoming deadlines</div>`;return}
|
|
const now=new Date();
|
|
let h='';
|
|
list.forEach(j=>{
|
|
const cd=j.closing_date;let days='',badge='neu';
|
|
if(cd){
|
|
const dt=new Date(cd+'T23:59:59');
|
|
const diff=Math.ceil((dt-now)/86400000);
|
|
if(diff<0){days='past due';badge='err'}
|
|
else if(diff===0){days='closes today';badge='err'}
|
|
else if(diff===1){days='closes tomorrow';badge='err'}
|
|
else if(diff<=3){days=`${diff} days`;badge='err'}
|
|
else if(diff<=7){days=`${diff} days`;badge='err'}
|
|
else days=`${diff} days`;
|
|
}
|
|
const s=j.salary?`<sp class="sal">$${(j.salary/1000).toFixed(0)}k</sp>`:'';
|
|
const emoji=badge==='err'?'🔴':'⚠️';
|
|
const nm=es(j.name);
|
|
const roleName=es(j.role);
|
|
const link=j.url?`<a href="${es(j.url)}" target="_blank" rel="noopener" style="color:var(--amber-bright);text-decoration:none;border-bottom:1px dashed var(--amber-faint)">${nm}</a>`:nm;
|
|
h+=`<div class="role-line"><div class="co">${emoji} ${link}</div>
|
|
<div class="ti">${roleName}${cd?` · closes ${cd}`:''}</div>
|
|
<div class="tg">${days?`<sp class="${badge}">${days}</sp>`:''}${s}${j.strong_match?`<sp class="ok">★ strong</sp>`:''}</div></div>`;
|
|
});
|
|
b.innerHTML=h;
|
|
if(list.length>5)b.innerHTML+=`<div style="font-size:10px;color:var(--amber-dim);margin-top:4px;text-align:center">${list.length} closing tracked · <a href="/jobs/" style="color:var(--amber);text-decoration:underline;text-underline-offset:2px">view all on dashboard →</a></div>`
|
|
}
|
|
|
|
function rWeather(d,b){
|
|
const w=d.weather||{};
|
|
if(w.error){b.innerHTML=`<div class="empty">weather offline</div>`;return}
|
|
b.innerHTML=`
|
|
<div class="w-row">
|
|
<div class="lft"><span style="font-size:28px">${wi(w.condition)}</span><div><div class="t">${w.temp_c}°</div><div class="c">${w.condition}</div></div></div>
|
|
<div class="rgt"><div class="b">${w.tomorrow_high}° / ${w.tomorrow_low}°</div><div>${wi(w.tomorrow_condition)} ${w.tomorrow_condition}</div></div>
|
|
</div>
|
|
<div style="font-size:10px;color:var(--amber-dim);margin-top:4px">wind ${w.wind_kmh}km/h · humidity ${w.humidity}%</div>`
|
|
}
|
|
|
|
function rHealth(d,b){
|
|
const c=d.cron_health||{};if(c.error){b.innerHTML=`<div class="empty">health unavailable</div>`;return}
|
|
const e=c.errors||0
|
|
b.innerHTML=`<div class="wdg">${e>0?`<sp class="err">⚠ ${e} error${e!==1?'s':''}</sp>`:`<sp class="ok">✓ all healthy</sp>`}<sp class="neu">${c.recent_runs||0} runs</sp>${(c.running||0)>0?`<sp class="err">⟳ ${c.running} running</sp>`:''}</div>`
|
|
}
|
|
|
|
function rMosq(d,b){
|
|
const day=Math.floor((Date.now()-new Date(new Date().getFullYear(),0,0))/86400000);
|
|
b.innerHTML=`<div class="mosq-box"><div class="b">🦟</div><div class="m">"${gM(day)}"</div><div class="z">Green Jacket Society · est. 2026</div></div>`
|
|
}
|
|
|
|
function rRhino(d,b){
|
|
const day=Math.floor((Date.now()-new Date(new Date().getFullYear(),0,0))/86400000);
|
|
b.innerHTML=`<div class="rhn-box"><div class="i">🚬</div><div class="m">"${gR(day)}"</div><div class="s">— The Rhino in the Green Jacket</div></div>`
|
|
}
|
|
|
|
/* ── Boot ───────────────────────────────────────────────── */
|
|
async function load(){
|
|
const today=new Date().toISOString().slice(0,10);
|
|
let r=await fetch(`/evening/${today}.json`).catch(()=>null);
|
|
if((!r||!r.ok))r=await fetch('/evening/latest.json').catch(()=>null);
|
|
if(!r||!r.ok){
|
|
document.getElementById('cards').innerHTML=`
|
|
<div style="text-align:center;padding:40px 20px;border:1px solid var(--amber-faint);margin-top:6px">
|
|
<div style="font-size:36px;margin-bottom:8px;opacity:0.3">🌆</div>
|
|
<div style="font-size:13px;color:var(--amber-dim)">no briefing yet today</div>
|
|
<div style="font-size:10px;color:var(--muted);margin-top:4px">check back after 18:45 AWST</div>
|
|
</div>`;
|
|
return
|
|
}
|
|
const d=await r.json();const dt=new Date(d.timestamp);
|
|
const dateStr=dt.toLocaleDateString('en-AU',{day:'numeric',month:'long',year:'numeric'});
|
|
const dayName=dt.toLocaleDateString('en-AU',{weekday:'long'});
|
|
|
|
// Typewriter the command line, then reveal content
|
|
const cmd=`./briefing --mode=stand-down --date=${d.timestamp.slice(0,10)}`;
|
|
typewriteFull(cmd,25,()=>{
|
|
document.getElementById('greetingLine').textContent=`Evening Stand-Down — ${dayName}`;
|
|
document.getElementById('dateLine').textContent=`${dateStr} AWST`;
|
|
document.getElementById('progressLabel').style.display='';
|
|
document.getElementById('footerCmd').textContent=`last brief — ${dt.toLocaleDateString('en-AU',{weekday:'short',day:'numeric',month:'short'})}`;
|
|
});
|
|
|
|
const cards=document.getElementById('cards');
|
|
const sections=[
|
|
['📊','recap — today\'s numbers',rRecap,1],
|
|
['🆕','new matches — today\'s additions',rRoles,2],
|
|
['⚠️','closing soon — urgent deadlines',rClosing,3],
|
|
['🌤️','weather — perth forecast',rWeather,4],
|
|
['⚙️','health — homelab pulse',rHealth,5],
|
|
['🦟','mosquito report — threat brief',rMosq,6],
|
|
['🚬','rhino\'s corner — words of wisdom',rRhino,7],
|
|
];
|
|
sections.forEach(([i,t,fn,dl])=>cards.appendChild(mkSect(i,t,fn,dl,d)))
|
|
}
|
|
load();
|
|
</script>
|
|
</body>
|
|
</html> |