2026-04-14 20:00:31 +00:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > Service Status< / title >
< link rel = "preconnect" href = "https://fonts.googleapis.com" >
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin >
< link href = "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel = "stylesheet" >
< style >
:root {
--bg: #ffffff; --surface: #f8fafb; --fg: #1a202c; --fg2: #64748b; --fg3: #94a3b8;
--border: #e2e8f0; --hover: #f1f5f9;
--green: #22c55e; --red: #ef4444; --amber: #f59e0b; --indigo: #6366f1;
--green-bg: #f0fdf4; --red-bg: #fef2f2; --amber-bg: #fffbeb;
--sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f172a; --surface: #1e293b; --fg: #e2e8f0; --fg2: #94a3b8; --fg3: #64748b;
--border: #334155; --hover: #1e293b;
--green: #4ade80; --red: #f87171; --amber: #fbbf24; --indigo: #818cf8;
--green-bg: #052e16; --red-bg: #450a0a; --amber-bg: #451a03;
}
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: var(--sans); background: var(--bg); color: var(--fg); line-height: 1.5; -webkit-font-smoothing: antialiased; font-size: 14px; }
.wrap { max-width: 720px; margin: 0 auto; padding: 32px 20px 64px; }
header { margin-bottom: 28px; }
header h1 { font-size: 20px; font-weight: 600; margin-bottom: 2px; }
.ts { color: var(--fg3); font-family: var(--mono); font-size: 12px; }
.hero { display: flex; align-items: center; gap: 10px; padding: 16px 20px; border-radius: 10px; margin-bottom: 24px; font-weight: 600; font-size: 15px; color: #fff; }
.hero-ok { background: var(--green); }
.hero-warn { background: var(--amber); color: var(--fg); }
.hero-down { background: var(--red); }
.hero-dot { width: 10px; height: 10px; border-radius: 50%; background: rgba(255,255,255,0.5); flex-shrink: 0; }
.hero-ok .hero-dot { animation: pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { transform: scale(1); opacity: 0.5; } 50% { transform: scale(1.4); opacity: 1; } }
.stale { background: var(--amber-bg); color: var(--amber); padding: 10px 16px; border-radius: 8px; font-size: 13px; margin-bottom: 16px; display: none; border: 1px solid color-mix(in srgb, var(--amber) 20%, transparent); }
/* Incidents */
.incidents { margin-bottom: 24px; }
.inc-header { font-size: 15px; font-weight: 600; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.inc-header .cnt { font-size: 12px; color: var(--fg3); font-weight: 400; }
.resolved-header { margin-top: 20px; }
.inc { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; margin-bottom: 10px; overflow: hidden; }
.inc-top { padding: 14px 16px; cursor: pointer; display: flex; align-items: flex-start; gap: 10px; user-select: none; }
.inc-top:hover { background: var(--hover); }
.sev { font-family: var(--mono); font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 4px; flex-shrink: 0; text-transform: uppercase; margin-top: 2px; }
.sev-1 { background: var(--red-bg); color: var(--red); border: 1px solid color-mix(in srgb, var(--red) 30%, transparent); }
.sev-2 { background: var(--amber-bg); color: var(--amber); border: 1px solid color-mix(in srgb, var(--amber) 30%, transparent); }
.sev-3 { background: var(--surface); color: var(--fg2); border: 1px solid var(--border); }
.inc-info { flex: 1; min-width: 0; }
.inc-title { font-size: 14px; font-weight: 600; }
.inc-meta { font-size: 12px; color: var(--fg3); margin-top: 2px; display: flex; gap: 12px; flex-wrap: wrap; }
.inc-services { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 6px; }
.inc-svc { font-size: 11px; padding: 1px 8px; border-radius: 4px; background: var(--hover); border: 1px solid var(--border); color: var(--fg2); }
.inc-tl { border-top: 1px solid var(--border); padding: 12px 16px; display: none; }
.inc.open .inc-tl { display: block; }
.tl-entry { position: relative; padding-left: 20px; padding-bottom: 14px; border-left: 2px solid var(--border); margin-left: 4px; }
.tl-entry:last-child { padding-bottom: 0; }
.tl-entry::before { content: ''; position: absolute; left: -5px; top: 4px; width: 8px; height: 8px; border-radius: 50%; background: var(--fg3); border: 2px solid var(--surface); }
.tl-time { font-family: var(--mono); font-size: 11px; color: var(--fg3); }
.tl-status { font-size: 12px; font-weight: 600; color: var(--fg); display: inline; }
.tl-body { font-size: 13px; color: var(--fg2); margin-top: 2px; white-space: pre-wrap; word-break: break-word; }
.inc-links { margin-top: 10px; font-size: 12px; display: flex; gap: 14px; }
.inc-links a { color: var(--indigo); text-decoration: none; }
.inc-links a:hover { text-decoration: underline; }
.inc-resolved { opacity: 0.7; }
.inc-resolved:hover { opacity: 1; }
.sev-ur { background: color-mix(in srgb, var(--indigo) 15%, transparent); color: var(--indigo); border: 1px solid color-mix(in srgb, var(--indigo) 30%, transparent); }
.report-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; border-radius: 10px; margin-bottom: 24px; background: var(--surface); border: 1px solid var(--border); }
.report-bar span { font-size: 13px; color: var(--fg2); }
.report-btn { font-family: var(--sans); font-size: 12px; font-weight: 600; padding: 6px 16px; border-radius: 6px; background: var(--indigo); color: #fff; text-decoration: none; white-space: nowrap; transition: opacity 0.15s; }
.report-btn:hover { opacity: 0.85; }
.bar { display: flex; gap: 6px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
.bar label { font-size: 11px; color: var(--fg3); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 500; }
.fbtn { font-family: var(--sans); font-size: 12px; padding: 4px 12px; border-radius: 6px; border: 1px solid var(--border); background: transparent; color: var(--fg2); cursor: pointer; font-weight: 500; }
.fbtn:hover { border-color: var(--fg3); color: var(--fg); }
.fbtn.on { background: var(--fg); color: var(--bg); border-color: var(--fg); }
.bar select { font-family: var(--sans); font-size: 12px; padding: 4px 8px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--fg); cursor: pointer; }
.g { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; margin-bottom: 12px; overflow: hidden; }
.g.hide { display: none; }
.gh { padding: 14px 16px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; user-select: none; }
.gh:hover { background: var(--hover); }
.gt { font-weight: 600; font-size: 13px; display: flex; align-items: center; gap: 8px; }
.gt .n { font-weight: 400; color: var(--fg3); font-size: 12px; }
.chev { font-size: 10px; color: var(--fg3); transition: transform 0.15s; display: inline-block; }
.g.shut .chev { transform: rotate(-90deg); }
.g.shut .gb { display: none; }
.gs { font-family: var(--mono); font-size: 12px; display: flex; gap: 8px; }
.gb { border-top: 1px solid var(--border); }
.colh { display: flex; align-items: center; padding: 6px 16px; gap: 8px; }
.colh-sp { width: 8px; flex-shrink: 0; }
.colh-n { flex: 1; font-size: 10px; color: var(--fg3); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 500; }
.colh-v { display: flex; gap: 2px; }
.colh-l { width: 52px; text-align: right; font-size: 10px; color: var(--fg3); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 500; }
.row { display: flex; align-items: center; padding: 8px 16px; gap: 8px; border-top: 1px solid var(--border); }
.row:first-of-type { border-top: none; }
.row:hover { background: var(--hover); }
.row.hide { display: none; }
.d { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.d-up { background: var(--green); }
.d-dn { background: var(--red); box-shadow: 0 0 0 3px rgba(239,68,68,0.15); }
.d-pn { background: var(--amber); }
.mn { flex: 1; font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mn a { color: inherit; text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.15s; }
.mn a:hover { color: var(--indigo); border-bottom-color: var(--indigo); }
.uv { display: flex; gap: 2px; font-family: var(--mono); font-size: 12px; }
.uv span { width: 52px; text-align: right; color: var(--fg3); }
.uv .ok { color: var(--green); }
.uv .wn { color: var(--amber); }
.uv .bd { color: var(--red); }
footer { color: var(--fg3); font-size: 11px; margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--border); text-align: center; }
.ld { text-align: center; padding: 60px 0; color: var(--fg3); }
.err { text-align: center; padding: 40px 0; color: var(--red); }
@media (max-width: 480px) {
.wrap { padding: 20px 14px 40px; }
.uv span, .colh-l { width: 42px; font-size: 11px; }
.row, .colh { padding-left: 12px; padding-right: 12px; }
.gh { padding: 12px; }
.inc-top { padding: 12px; }
}
< / style >
< / head >
< body >
< div class = "wrap" >
< header >
< h1 > Service Status< / h1 >
< div class = "ts" id = "ts" > < / div >
< / header >
< div class = "stale" id = "stale" > < / div >
< div class = "hero" id = "hero" > < / div >
< div class = "report-bar" >
< span > Something not working?< / span >
< a href = "https://github.com/ViktorBarzin/infra/issues/new?template=outage-report.yml" target = "_blank" rel = "noopener" class = "report-btn" > Report an Outage< / a >
< / div >
< div id = "incidents" > < / div >
< div class = "bar" id = "bar" style = "display:none" >
< label > Show:< / label >
< button class = "fbtn on" data-f = "all" > All< / button >
< button class = "fbtn" data-f = "up" > Up< / button >
< button class = "fbtn" data-f = "down" > Down< / button >
< span style = "flex:1" > < / span >
< label > Sort:< / label >
< select id = "ss" >
< option value = "status" > Status< / option >
< option value = "name" > Name< / option >
< option value = "u-asc" > Uptime asc< / option >
< option value = "u-desc" > Uptime desc< / option >
< / select >
< / div >
< div id = "gs" > < div class = "ld" > Loading… < / div > < / div >
< footer > Updated every 5 minutes · Powered by Uptime Kuma · < a href = "https://github.com/ViktorBarzin/infra/issues" style = "color:var(--fg3)" > Report issues< / a > < / footer >
< / div >
< script >
(function(){
var U='status.json',S=6e5,D=null,F='all',O='status';
function esc(s){var d=document.createElement('div');d.textContent=s||'';return d.innerHTML}
function ago(d){var s=Math.floor((Date.now()-d)/1e3);if(s< 0 ) s = 0;return s < 60 ? s + ' s ago ' :s < 3600 ? Math . floor ( s / 60 ) + ' m ago ' :s < 86400 ? Math . floor ( s / 3600 ) + ' h ago ' :Math . floor ( s / 86400 ) + ' d ago ' }
function dur(start,end){var m=Math.floor((end-start)/6e4);if(m< 1 ) return ' < 1m ' ; return m < 60 ? m + ' m ' :Math . floor ( m / 60 ) + ' h ' + m % 60 + ' m ' }
function uc(p){return p==null?'':p>=99?'ok':p>=95?'wn':'bd'}
function pf(p){return p==null?'\u2014':p.toFixed(1)+'%'}
function srt(a){return a.slice().sort(function(x,y){
if(O==='name')return x.name.localeCompare(y.name);
if(O==='u-asc'){var xa=x.uptime_24h==null?101:x.uptime_24h,ya=y.uptime_24h==null?101:y.uptime_24h;return xa-ya}
if(O==='u-desc'){var xd=x.uptime_24h==null?-1:x.uptime_24h,yd=y.uptime_24h==null?-1:y.uptime_24h;return yd-xd}
var o={down:0,pending:1,up:2},ao=o[x.status]!=null?o[x.status]:1,bo=o[y.status]!=null?o[y.status]:1;
return ao!==bo?ao-bo:x.name.localeCompare(y.name);
})}
function fm(m){return F==='all'||(F==='up'?m.status==='up':m.status!=='up')}
function buildIncident(inc,resolved){
var isReport=inc.type==='user-report';
var sevNum=isReport?0:inc.severity==='sev1'?1:inc.severity==='sev2'?2:3;
var created=new Date(inc.created_at);
var end=resolved& & inc.closed_at?new Date(inc.closed_at):new Date();
var el=document.createElement('div');
el.className='inc'+(resolved?' inc-resolved':'');
// Top bar
var top=document.createElement('div');
top.className='inc-top';
var badgeHtml=isReport
?'< div class = "sev sev-ur" > REPORT< / div > '
:'< div class = "sev sev-'+sevNum+'" > SEV'+sevNum+'< / div > ';
var html=badgeHtml;
html+='< div class = "inc-info" > < div class = "inc-title" > '+esc(inc.title)+'< / div > ';
html+='< div class = "inc-meta" > < span > '+ago(created)+'< / span > ';
if(!isReport)html+='< span > '+dur(created,end)+'< / span > ';
feat: augment outage report template with debugging context
- Expand service list: add Home Assistant, Actual Budget, Audiobookshelf,
Linkwarden, Matrix, Paperless, Tandoor, FreshRSS, Frigate, HackMD,
Excalidraw, Wealthfolio, Send, Stirling PDF
- Add structured debugging fields: error type, scope (just me vs others),
when it started, URL accessed
- Fix user report parser to extract all form fields into status.json
- Show error type, scope, and start time in status page report cards
[ci skip]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:03:44 +00:00
if(isReport&&inc.error_type)html+='<span>'+esc(inc.error_type)+'</span>';
if(isReport&&inc.scope)html+='<span>'+esc(inc.scope)+'</span>';
if(isReport& & inc.when_started)html+='< span > Since: '+esc(inc.when_started)+'< / span > ';
2026-04-14 20:00:31 +00:00
if(resolved)html+='< span style = "color:var(--green)" > Resolved< / span > ';
html+='< / div > ';
if(inc.affected_services& & inc.affected_services.length){
html+='< div class = "inc-services" > ';
for(var i=0;i< inc.affected_services.length ; i + + ) html + = ' < span class = "inc-svc" > '+esc(inc.affected_services[i])+'< / span > ';
html+='< / div > ';
}
html+='< / div > < span class = "chev" > ▸ < / span > ';
top.innerHTML=html;
top.onclick=function(){el.classList.toggle('open')};
el.appendChild(top);
// Timeline
var tl=document.createElement('div');
tl.className='inc-tl';
if(inc.timeline& & inc.timeline.length){
for(var i=inc.timeline.length-1;i>=0;i--){
var te=inc.timeline[i];
var entry=document.createElement('div');
entry.className='tl-entry';
entry.innerHTML='< div class = "tl-time" > '+new Date(te.timestamp).toLocaleString()+'< / div > '
+'< div class = "tl-status" > '+esc(te.status)+'< / div > '
+'< div class = "tl-body" > '+esc(te.body)+'< / div > ';
tl.appendChild(entry);
}
}
// Links
var links=document.createElement('div');
links.className='inc-links';
if(inc.postmortem)links.innerHTML+='< a href = "'+esc(inc.postmortem)+'" target = "_blank" rel = "noopener" > Postmortem< / a > ';
links.innerHTML+='< a href = "'+esc(inc.url)+'" target = "_blank" rel = "noopener" > View on GitHub → < / a > ';
tl.appendChild(links);
el.appendChild(tl);
return el;
}
function render(data){
D=data;
var t=new Date(data.last_updated),age=Date.now()-t.getTime();
document.getElementById('ts').textContent=ago(t);
var st=document.getElementById('stale');
if(age>S){st.textContent='Data is '+Math.floor(age/6e4)+' minutes old. Monitoring may be unreachable.';st.style.display='block'}else st.style.display='none';
var gs={};
for(var gn in data.groups){var a=data.groups[gn].filter(function(m){return m.status!=='paused'});if(a.length)gs[gn]=a}
var tu=0,td=0;
for(var g in gs)for(var i=0;i< gs [ g ] . length ; i + + ) gs [ g ] [ i ] . status = =='up'?tu++:td++;
// Incidents
var inc=data.incidents||{active:[],resolved:[]};
var incEl=document.getElementById('incidents');
incEl.innerHTML='';
// Hero — incidents take priority
var h=document.getElementById('hero');
if(inc.active.length>0){
var maxSev=3;
for(var si=0;si< inc.active.length ; si + + ) {
var s=inc.active[si].severity==='sev1'?1:inc.active[si].severity==='sev2'?2:3;
if(s< maxSev ) maxSev = s;
}
if(maxSev===1){h.className='hero hero-down';h.innerHTML='< div class = "hero-dot" > < / div > Active Incident \u2014 SEV1'}
else{h.className='hero hero-warn';h.innerHTML='< div class = "hero-dot" > < / div > '+inc.active.length+' Active Incident'+(inc.active.length>1?'s':'')}
}else if(!td){h.className='hero hero-ok';h.innerHTML='< div class = "hero-dot" > < / div > All Systems Operational'}
else if(td< =3){h.className='hero hero-warn';h.innerHTML='< div class = "hero-dot" > < / div > '+td+' service'+(td>1?'s':'')+' experiencing issues'}
else{h.className='hero hero-down';h.innerHTML='< div class = "hero-dot" > < / div > '+td+' services down'}
// Render active incidents
if(inc.active.length>0){
var ah=document.createElement('div');
ah.className='inc-header';
ah.innerHTML='Active Incidents < span class = "cnt" > '+inc.active.length+'< / span > ';
incEl.appendChild(ah);
for(var ai=0;ai< inc.active.length ; ai + + ) incEl . appendChild ( buildIncident ( inc . active [ ai ] , false ) ) ;
}
// Render user reports
var reports=inc.user_reports||[];
if(reports.length>0){
var urh=document.createElement('div');
urh.className='inc-header';
urh.innerHTML='User Reports < span class = "cnt" > '+reports.length+'< / span > ';
incEl.appendChild(urh);
for(var ui=0;ui< reports.length ; ui + + ) incEl . appendChild ( buildIncident ( reports [ ui ] , false ) ) ;
}
// Render resolved incidents
if(inc.resolved.length>0){
var rh=document.createElement('div');
rh.className='inc-header resolved-header';
rh.innerHTML='Recently Resolved < span class = "cnt" > last 7 days< / span > ';
incEl.appendChild(rh);
for(var ri=0;ri< inc.resolved.length ; ri + + ) incEl . appendChild ( buildIncident ( inc . resolved [ ri ] , true ) ) ;
}
// Monitor groups
document.getElementById('bar').style.display='flex';
var c=document.getElementById('gs');c.innerHTML='';
var ks=Object.keys(gs).sort(function(a,b){return gs[b].length-gs[a].length});
for(var ki=0;ki< ks.length ; ki + + ) {
var gn=ks[ki],ms=gs[gn],so=srt(ms),vc=so.filter(fm).length;
var ge=document.createElement('div');ge.className='g'+(vc?'':' hide');
var up=ms.filter(function(m){return m.status==='up'}).length,dn=ms.length-up;
var hd=document.createElement('div');hd.className='gh';
hd.innerHTML='< div class = "gt" > < span class = "chev" > ▸ < / span > '+gn+' < span class = "n" > '+ms.length+'< / span > < / div > < div class = "gs" > '+(dn?'< span style = "color:var(--red)" > '+dn+' down< / span > ':'')+'< span style = "color:var(--green)" > '+up+' up< / span > < / div > ';
hd.onclick=function(){this.parentElement.classList.toggle('shut')};
var bd=document.createElement('div');bd.className='gb';
var ch=document.createElement('div');ch.className='colh';
ch.innerHTML='< div class = "colh-sp" > < / div > < div class = "colh-n" > Service< / div > < div class = "colh-v" > < div class = "colh-l" > 24h< / div > < div class = "colh-l" > 7d< / div > < div class = "colh-l" > 30d< / div > < / div > ';
bd.appendChild(ch);
for(var mi=0;mi< so.length ; mi + + ) {
var m=so[mi],dc=m.status==='up'?'d-up':m.status==='pending'?'d-pn':'d-dn';
var r=document.createElement('div');r.className='row'+(fm(m)?'':' hide');
var nameHtml=m.name;
if(m.url){nameHtml='< a href = "'+m.url+'" target = "_blank" rel = "noopener" > '+m.name+'< / a > '}
r.innerHTML='< div class = "d '+dc+'" > < / div > < div class = "mn" > '+nameHtml+'< / div > < div class = "uv" > < span class = "'+uc(m.uptime_24h)+'" > '+pf(m.uptime_24h)+'< / span > < span class = "'+uc(m.uptime_7d)+'" > '+pf(m.uptime_7d)+'< / span > < span class = "'+uc(m.uptime_30d)+'" > '+pf(m.uptime_30d)+'< / span > < / div > ';
bd.appendChild(r);
}
ge.appendChild(hd);ge.appendChild(bd);c.appendChild(ge);
}
}
document.addEventListener('click',function(e){
if(!e.target.classList.contains('fbtn'))return;
var bs=document.querySelectorAll('.fbtn');for(var i=0;i< bs.length ; i + + ) bs [ i ] . classList . remove ( ' on ' ) ;
e.target.classList.add('on');F=e.target.getAttribute('data-f');if(D)render(D);
});
document.getElementById('ss').onchange=function(){O=this.value;if(D)render(D)};
function load(){
fetch(U+'?t='+Date.now()).then(function(r){if(!r.ok)throw 0;return r.json()}).then(render)
.catch(function(){document.getElementById('gs').innerHTML='< div class = "err" > Could not load status data.< / div > ';
var h=document.getElementById('hero');h.className='hero hero-down';h.innerHTML='< div class = "hero-dot" > < / div > Status Unavailable'});
}
load();setInterval(load,6e4);
})();
< / script >
< / body >
< / html >