stem95su: scheduled Drive->site sync CronJob (every 10m)
CronJob stem95su-gdrive-sync (*/10) mounts the content PVC RW and rclone-syncs the read-only Drive folder "claude" (stem claude/files) onto it (rclone/rclone:1.74.3, scope=drive.readonly, empty-source guard + --max-delete 25). ESO ExternalSecret stem95su-rclone <- Vault secret/stem95su. Requires the GCP OAuth app published to Production or the refresh token expires ~weekly. Lands the gdrive-sync stack on master (it had landed on a feature branch by accident on the shared devvm checkout). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
05b50d2b96
commit
6d224861c4
1168 changed files with 120 additions and 358547 deletions
|
|
@ -1,359 +0,0 @@
|
|||
<!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>';
|
||||
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>';
|
||||
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>
|
||||
|
|
@ -1,621 +0,0 @@
|
|||
variable "tls_secret_name" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
variable "nfs_server" { type = string }
|
||||
|
||||
data "vault_kv_secret_v2" "viktor" {
|
||||
mount = "secret"
|
||||
name = "viktor"
|
||||
}
|
||||
|
||||
locals {
|
||||
index_html = file("${path.module}/index.html")
|
||||
}
|
||||
|
||||
resource "kubernetes_namespace_v1" "status_page" {
|
||||
metadata {
|
||||
name = "status-page"
|
||||
labels = {
|
||||
tier = local.tiers.aux
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_config_map_v1" "status_page_template" {
|
||||
metadata {
|
||||
name = "status-page-template"
|
||||
namespace = kubernetes_namespace_v1.status_page.metadata[0].name
|
||||
}
|
||||
data = {
|
||||
"index.html" = local.index_html
|
||||
"CNAME" = "status.viktorbarzin.me"
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_service_account_v1" "status_page" {
|
||||
metadata {
|
||||
name = "status-page-pusher"
|
||||
namespace = kubernetes_namespace_v1.status_page.metadata[0].name
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_cluster_role_v1" "ingress_reader" {
|
||||
metadata {
|
||||
name = "status-page-ingress-reader"
|
||||
}
|
||||
rule {
|
||||
api_groups = ["networking.k8s.io"]
|
||||
resources = ["ingresses"]
|
||||
verbs = ["list"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_cluster_role_binding_v1" "ingress_reader" {
|
||||
metadata {
|
||||
name = "status-page-ingress-reader"
|
||||
}
|
||||
role_ref {
|
||||
api_group = "rbac.authorization.k8s.io"
|
||||
kind = "ClusterRole"
|
||||
name = kubernetes_cluster_role_v1.ingress_reader.metadata[0].name
|
||||
}
|
||||
subject {
|
||||
kind = "ServiceAccount"
|
||||
name = kubernetes_service_account_v1.status_page.metadata[0].name
|
||||
namespace = kubernetes_namespace_v1.status_page.metadata[0].name
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Status Page Pusher ── DISABLED 2026-05-26
|
||||
# Reads Uptime Kuma monitors, generates status.json, pushes to GitHub Pages.
|
||||
#
|
||||
# Disabled because per-invocation `apk add git` + `pip install uptime-kuma-api`
|
||||
# was hammering the Proxmox sdc thin pool (~3.2 MB/s of the ~8 MB/s sustained
|
||||
# host-side, ~804 GB written over 18 h). Re-enable with a custom image that
|
||||
# bakes git + uptime-kuma-api so cold-install is gone.
|
||||
# =============================================================================
|
||||
# resource "kubernetes_cron_job_v1" "status_page_pusher" {
|
||||
# metadata {
|
||||
# name = "status-page-pusher"
|
||||
# namespace = kubernetes_namespace_v1.status_page.metadata[0].name
|
||||
# }
|
||||
# spec {
|
||||
# concurrency_policy = "Forbid"
|
||||
# failed_jobs_history_limit = 3
|
||||
# successful_jobs_history_limit = 3
|
||||
# schedule = "*/5 * * * *"
|
||||
# job_template {
|
||||
# metadata {}
|
||||
# spec {
|
||||
# backoff_limit = 1
|
||||
# ttl_seconds_after_finished = 300
|
||||
# template {
|
||||
# metadata {}
|
||||
# spec {
|
||||
# service_account_name = kubernetes_service_account_v1.status_page.metadata[0].name
|
||||
# container {
|
||||
# name = "status-pusher"
|
||||
# image = "docker.io/library/python:3.12-alpine"
|
||||
# command = ["/bin/sh", "-c", <<-EOT
|
||||
# apk add --no-cache git >/dev/null 2>&1
|
||||
# pip install --quiet --disable-pip-version-check uptime-kuma-api
|
||||
# python3 << 'PYEOF'
|
||||
# import os, sys, json, time, subprocess
|
||||
# from datetime import datetime, timezone, timedelta
|
||||
# from uptime_kuma_api import UptimeKumaApi
|
||||
#
|
||||
# UPTIME_KUMA_URL = "http://uptime-kuma.uptime-kuma.svc.cluster.local"
|
||||
# UPTIME_KUMA_PASS = os.environ["UPTIME_KUMA_PASSWORD"]
|
||||
# GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
|
||||
# REPO = "ViktorBarzin/status-page"
|
||||
# REPO_URL = "https://" + GITHUB_TOKEN + "@github.com/" + REPO + ".git"
|
||||
#
|
||||
# TYPE_NAMES = {
|
||||
# "http": "HTTP",
|
||||
# "port": "TCP Port",
|
||||
# "ping": "Ping",
|
||||
# "keyword": "HTTP Keyword",
|
||||
# "grpc-keyword": "gRPC",
|
||||
# "dns": "DNS",
|
||||
# "docker": "Docker",
|
||||
# "push": "Push",
|
||||
# "steam": "Steam",
|
||||
# "gamedig": "GameDig",
|
||||
# "mqtt": "MQTT",
|
||||
# "sqlserver": "SQL Server",
|
||||
# "postgres": "PostgreSQL",
|
||||
# "mysql": "MySQL",
|
||||
# "mongodb": "MongoDB",
|
||||
# "radius": "RADIUS",
|
||||
# "redis": "Redis",
|
||||
# "tailscale-ping": "Tailscale Ping",
|
||||
# "real-browser": "Real Browser",
|
||||
# "group": "Group",
|
||||
# "snmp": "SNMP",
|
||||
# "json-query": "JSON Query",
|
||||
# }
|
||||
#
|
||||
# def beat_status_is_up(status_val):
|
||||
# """Handle both enum and int status values."""
|
||||
# if hasattr(status_val, "value"):
|
||||
# return status_val.value == 1
|
||||
# return status_val == 1
|
||||
#
|
||||
# # Build namespace -> external URL map from K8s ingresses
|
||||
# ingress_map = {}
|
||||
# try:
|
||||
# import ssl, urllib.request
|
||||
# token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||
# ca_path = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||
# if os.path.exists(token_path):
|
||||
# with open(token_path) as f:
|
||||
# token = f.read().strip()
|
||||
# ctx = ssl.create_default_context(cafile=ca_path)
|
||||
# k8s_host = os.environ.get("KUBERNETES_SERVICE_HOST", "kubernetes.default.svc")
|
||||
# k8s_port = os.environ.get("KUBERNETES_SERVICE_PORT", "443")
|
||||
# req = urllib.request.Request(
|
||||
# "https://" + k8s_host + ":" + k8s_port + "/apis/networking.k8s.io/v1/ingresses",
|
||||
# headers={"Authorization": "Bearer " + token}
|
||||
# )
|
||||
# resp = urllib.request.urlopen(req, context=ctx, timeout=10)
|
||||
# ing_data = json.loads(resp.read())
|
||||
# for item in ing_data.get("items", []):
|
||||
# ns = item["metadata"]["namespace"]
|
||||
# rules = item.get("spec", {}).get("rules", [])
|
||||
# if rules and rules[0].get("host"):
|
||||
# host = rules[0]["host"]
|
||||
# if ns not in ingress_map:
|
||||
# ingress_map[ns] = "https://" + host
|
||||
# print(f"Built ingress map: {len(ingress_map)} namespaces")
|
||||
# except Exception as e:
|
||||
# print(f"Warning: could not build ingress map: {e}")
|
||||
#
|
||||
# print("Connecting to Uptime Kuma...")
|
||||
# api = UptimeKumaApi(UPTIME_KUMA_URL, timeout=30)
|
||||
# api.login("admin", UPTIME_KUMA_PASS)
|
||||
#
|
||||
# monitors = api.get_monitors()
|
||||
# print(f"Fetched {len(monitors)} monitors")
|
||||
#
|
||||
# # Get current heartbeats for live status
|
||||
# heartbeats = api.get_heartbeats()
|
||||
#
|
||||
# now = datetime.now(timezone.utc)
|
||||
#
|
||||
# def calc_uptime(beat_list, hours):
|
||||
# cutoff = now - timedelta(hours=hours)
|
||||
# relevant = []
|
||||
# for b in beat_list:
|
||||
# t = str(b["time"])
|
||||
# try:
|
||||
# bt = datetime.fromisoformat(t.replace("Z", "+00:00"))
|
||||
# except (ValueError, TypeError):
|
||||
# continue
|
||||
# if bt.tzinfo is None:
|
||||
# bt = bt.replace(tzinfo=timezone.utc)
|
||||
# if bt > cutoff:
|
||||
# relevant.append(b)
|
||||
# if not relevant:
|
||||
# return None
|
||||
# up_count = sum(1 for b in relevant if beat_status_is_up(b.get("status", 0)))
|
||||
# return round(up_count / len(relevant) * 100, 1)
|
||||
#
|
||||
# groups = {}
|
||||
# for m in monitors:
|
||||
# raw_type = m.get("type", "unknown")
|
||||
# monitor_type = raw_type.value if hasattr(raw_type, "value") else str(raw_type)
|
||||
# monitor_type = monitor_type.lower().replace("monitortype.", "")
|
||||
# if m["name"].startswith("[External] "):
|
||||
# group_name = "External Reachability"
|
||||
# else:
|
||||
# group_name = TYPE_NAMES.get(monitor_type, monitor_type.upper())
|
||||
#
|
||||
# if not m.get("active", True):
|
||||
# continue
|
||||
# else:
|
||||
# # Get latest heartbeat for current status
|
||||
# mid = m["id"]
|
||||
# mon_beats = heartbeats.get(mid, heartbeats.get(str(mid), []))
|
||||
# if mon_beats:
|
||||
# # Flatten nested lists (API format varies by version)
|
||||
# flat = []
|
||||
# for item in mon_beats:
|
||||
# if isinstance(item, list):
|
||||
# flat.extend(item)
|
||||
# elif isinstance(item, dict):
|
||||
# flat.append(item)
|
||||
# mon_beats = flat if flat else mon_beats
|
||||
# latest = mon_beats[-1] if mon_beats else None
|
||||
# if latest and isinstance(latest, dict) and beat_status_is_up(latest.get("status", 0)):
|
||||
# status = "up"
|
||||
# else:
|
||||
# status = "down"
|
||||
# else:
|
||||
# status = "pending"
|
||||
#
|
||||
# uptime_24h = None
|
||||
# uptime_7d = None
|
||||
# uptime_30d = None
|
||||
# try:
|
||||
# beats = api.get_monitor_beats(m["id"], 720)
|
||||
# if beats:
|
||||
# uptime_24h = calc_uptime(beats, 24)
|
||||
# uptime_7d = calc_uptime(beats, 168)
|
||||
# uptime_30d = calc_uptime(beats, 720)
|
||||
# except Exception as e:
|
||||
# print(f" Warning: could not get beats for {m['name']}: {e}")
|
||||
#
|
||||
# if group_name not in groups:
|
||||
# groups[group_name] = []
|
||||
#
|
||||
# # Extract external URL for HTTP monitors
|
||||
# monitor_url = None
|
||||
# raw_url = m.get("url", "") or ""
|
||||
# if monitor_type == "http" and raw_url:
|
||||
# if ".svc.cluster.local" not in raw_url and raw_url.startswith("http"):
|
||||
# monitor_url = raw_url.rstrip("/")
|
||||
# else:
|
||||
# # Internal URL — derive external from namespace
|
||||
# import re as _re
|
||||
# ns_match = _re.search(r"//[^.]+\.([^.]+)\.svc\.cluster\.local", raw_url)
|
||||
# if ns_match:
|
||||
# ns = ns_match.group(1)
|
||||
# if ns in ingress_map:
|
||||
# monitor_url = ingress_map[ns]
|
||||
#
|
||||
# entry = {
|
||||
# "name": m["name"],
|
||||
# "status": status,
|
||||
# "uptime_24h": uptime_24h,
|
||||
# "uptime_7d": uptime_7d,
|
||||
# "uptime_30d": uptime_30d,
|
||||
# }
|
||||
# if monitor_url:
|
||||
# entry["url"] = monitor_url
|
||||
#
|
||||
# groups[group_name].append(entry)
|
||||
#
|
||||
# api.disconnect()
|
||||
# print(f"Generated {len(groups)} groups")
|
||||
#
|
||||
# # ============ Detect external-down / internal-up divergence ============
|
||||
# external_status = {}
|
||||
# internal_status = {}
|
||||
# for gname, gmonitors in groups.items():
|
||||
# for mon in gmonitors:
|
||||
# if mon["name"].startswith("[External] "):
|
||||
# svc = mon["name"].replace("[External] ", "").lower()
|
||||
# external_status[svc] = mon["status"]
|
||||
# elif gname != "External Reachability":
|
||||
# internal_status[mon["name"].lower()] = mon["status"]
|
||||
#
|
||||
# divergent = []
|
||||
# for svc, ext_st in external_status.items():
|
||||
# if ext_st != "down":
|
||||
# continue
|
||||
# for iname, int_st in internal_status.items():
|
||||
# if svc in iname or iname in svc:
|
||||
# if int_st == "up":
|
||||
# divergent.append(svc)
|
||||
# break
|
||||
#
|
||||
# divergence_count = len(divergent)
|
||||
# metric_body = (
|
||||
# "# HELP external_internal_divergence_count Services externally down but internally up\n"
|
||||
# "# TYPE external_internal_divergence_count gauge\n"
|
||||
# f"external_internal_divergence_count {divergence_count}\n"
|
||||
# )
|
||||
# for svc in divergent:
|
||||
# metric_body += f'external_internal_divergence_services{{service="{svc}"}} 1\n'
|
||||
#
|
||||
# try:
|
||||
# import urllib.request as _ur
|
||||
# req = _ur.Request(
|
||||
# "http://prometheus-prometheus-pushgateway.monitoring:9091/metrics/job/external-monitor-divergence",
|
||||
# data=metric_body.encode(),
|
||||
# method="POST"
|
||||
# )
|
||||
# _ur.urlopen(req, timeout=10)
|
||||
# if divergent:
|
||||
# print(f"WARNING: {len(divergent)} services externally down but internally up: {divergent}")
|
||||
# else:
|
||||
# print("No external/internal divergence detected")
|
||||
# except Exception as e:
|
||||
# print(f"Warning: could not push divergence metric: {e}")
|
||||
#
|
||||
# # ============ Fetch incidents from GitHub Issues ============
|
||||
# import urllib.request, urllib.error, re as _re2
|
||||
#
|
||||
# def fetch_github_json(url):
|
||||
# req = urllib.request.Request(url, headers={
|
||||
# "Authorization": "token " + GITHUB_TOKEN,
|
||||
# "Accept": "application/vnd.github.v3+json",
|
||||
# "User-Agent": "status-page-pusher",
|
||||
# })
|
||||
# resp = urllib.request.urlopen(req, timeout=15)
|
||||
# return json.loads(resp.read())
|
||||
#
|
||||
# def parse_severity(labels):
|
||||
# for lbl in labels:
|
||||
# name = lbl["name"].lower()
|
||||
# if name in ("sev1", "sev2", "sev3"):
|
||||
# return name
|
||||
# return "sev3"
|
||||
#
|
||||
# def parse_affected_services(body):
|
||||
# services = []
|
||||
# if not body:
|
||||
# return services
|
||||
# in_section = False
|
||||
# for line in body.split("\n"):
|
||||
# stripped = line.strip()
|
||||
# if stripped.lower().startswith("## affected"):
|
||||
# in_section = True
|
||||
# continue
|
||||
# if in_section:
|
||||
# if stripped.startswith("##"):
|
||||
# break
|
||||
# if stripped.startswith("- ") and not stripped.startswith("- <!--"):
|
||||
# services.append(stripped[2:].strip())
|
||||
# return services
|
||||
#
|
||||
# def parse_timeline(comments):
|
||||
# timeline = []
|
||||
# for c in comments:
|
||||
# body = (c.get("body") or "").strip()
|
||||
# status_label = "Update"
|
||||
# if body.startswith("**"):
|
||||
# end = body.find("**", 2)
|
||||
# if end > 2:
|
||||
# status_label = body[2:end]
|
||||
# timeline.append({
|
||||
# "timestamp": c["created_at"],
|
||||
# "status": status_label,
|
||||
# "body": body,
|
||||
# })
|
||||
# return timeline
|
||||
#
|
||||
# def extract_postmortem(comments):
|
||||
# for c in reversed(comments):
|
||||
# body = (c.get("body") or "").lower()
|
||||
# if "postmortem" in body:
|
||||
# urls = _re2.findall(r'https?://\S+', c.get("body", ""))
|
||||
# if urls:
|
||||
# return urls[0].rstrip(")>")
|
||||
# return None
|
||||
#
|
||||
# incidents_active = []
|
||||
# incidents_resolved = []
|
||||
# user_reports = []
|
||||
#
|
||||
# ISSUES_REPO = "ViktorBarzin/infra"
|
||||
#
|
||||
# def has_label(issue, name):
|
||||
# return any(l["name"].lower() == name.lower() for l in issue.get("labels", []))
|
||||
#
|
||||
# def parse_form_field(body, heading):
|
||||
# """Extract value after a ### heading from GitHub Issue Form response."""
|
||||
# if not body:
|
||||
# return None
|
||||
# lines = body.split("\n")
|
||||
# for i, ln in enumerate(lines):
|
||||
# if heading.lower() in ln.lower() and ln.strip().startswith("#"):
|
||||
# for j in range(i + 1, len(lines)):
|
||||
# val = lines[j].strip()
|
||||
# if val and not val.startswith("#") and val != "_No response_":
|
||||
# return val
|
||||
# return None
|
||||
#
|
||||
# def parse_user_report_context(body):
|
||||
# """Extract structured fields from the issue form body."""
|
||||
# service = parse_form_field(body, "affected service")
|
||||
# # Strip parenthetical hints: "Nextcloud (files, calendar)" -> "Nextcloud"
|
||||
# if service and "(" in service:
|
||||
# service = service[:service.index("(")].strip()
|
||||
# return {
|
||||
# "service": service,
|
||||
# "error_type": parse_form_field(body, "what kind of error"),
|
||||
# "scope": parse_form_field(body, "is it just you"),
|
||||
# "when": parse_form_field(body, "when did it start"),
|
||||
# "url": parse_form_field(body, "url you were accessing"),
|
||||
# }
|
||||
#
|
||||
# try:
|
||||
# issues_url = "https://api.github.com/repos/" + ISSUES_REPO + "/issues"
|
||||
#
|
||||
# # Fetch admin-declared incidents (open)
|
||||
# open_incidents = fetch_github_json(
|
||||
# issues_url + "?labels=incident&state=open&per_page=50&sort=created&direction=desc"
|
||||
# )
|
||||
# for issue in open_incidents:
|
||||
# if issue.get("pull_request"):
|
||||
# continue
|
||||
# comments = fetch_github_json(issue["comments_url"]) if issue.get("comments", 0) > 0 else []
|
||||
# incidents_active.append({
|
||||
# "id": issue["number"],
|
||||
# "title": issue["title"],
|
||||
# "type": "incident",
|
||||
# "severity": parse_severity(issue.get("labels", [])),
|
||||
# "status": "active",
|
||||
# "created_at": issue["created_at"],
|
||||
# "updated_at": issue["updated_at"],
|
||||
# "affected_services": parse_affected_services(issue.get("body")),
|
||||
# "timeline": parse_timeline(comments),
|
||||
# "url": issue["html_url"],
|
||||
# "postmortem": None,
|
||||
# })
|
||||
#
|
||||
# # Fetch user reports (open, not yet triaged to incident)
|
||||
# open_reports = fetch_github_json(
|
||||
# issues_url + "?labels=user-report&state=open&per_page=20&sort=created&direction=desc"
|
||||
# )
|
||||
# for issue in open_reports:
|
||||
# if issue.get("pull_request"):
|
||||
# continue
|
||||
# if has_label(issue, "incident"):
|
||||
# continue # Already promoted to incident, skip duplicate
|
||||
# ctx = parse_user_report_context(issue.get("body"))
|
||||
# svc = ctx["service"]
|
||||
# user_reports.append({
|
||||
# "id": issue["number"],
|
||||
# "title": issue["title"],
|
||||
# "type": "user-report",
|
||||
# "status": "open",
|
||||
# "created_at": issue["created_at"],
|
||||
# "affected_services": [svc] if svc else [],
|
||||
# "error_type": ctx.get("error_type"),
|
||||
# "scope": ctx.get("scope"),
|
||||
# "when_started": ctx.get("when"),
|
||||
# "url": issue["html_url"],
|
||||
# })
|
||||
#
|
||||
# # Fetch recently closed incidents (last 7 days)
|
||||
# closed_incidents = fetch_github_json(
|
||||
# issues_url + "?labels=incident&state=closed&per_page=20&sort=updated&direction=desc"
|
||||
# )
|
||||
# cutoff_7d = (now - timedelta(days=7)).isoformat()
|
||||
# for issue in closed_incidents:
|
||||
# if issue.get("pull_request"):
|
||||
# continue
|
||||
# if issue.get("closed_at") and issue["closed_at"] < cutoff_7d:
|
||||
# continue
|
||||
# comments = fetch_github_json(issue["comments_url"]) if issue.get("comments", 0) > 0 else []
|
||||
# incidents_resolved.append({
|
||||
# "id": issue["number"],
|
||||
# "title": issue["title"],
|
||||
# "type": "incident",
|
||||
# "severity": parse_severity(issue.get("labels", [])),
|
||||
# "status": "resolved",
|
||||
# "created_at": issue["created_at"],
|
||||
# "closed_at": issue["closed_at"],
|
||||
# "updated_at": issue["updated_at"],
|
||||
# "affected_services": parse_affected_services(issue.get("body")),
|
||||
# "timeline": parse_timeline(comments),
|
||||
# "url": issue["html_url"],
|
||||
# "postmortem": extract_postmortem(comments),
|
||||
# })
|
||||
#
|
||||
# print(f"Incidents: {len(incidents_active)} active, {len(incidents_resolved)} resolved, {len(user_reports)} user reports")
|
||||
# except Exception as e:
|
||||
# print(f"Warning: could not fetch incidents: {e}")
|
||||
#
|
||||
# status_data = {
|
||||
# "last_updated": now.isoformat(),
|
||||
# "groups": groups,
|
||||
# "incidents": {
|
||||
# "active": incidents_active,
|
||||
# "resolved": incidents_resolved,
|
||||
# "user_reports": user_reports,
|
||||
# },
|
||||
# }
|
||||
#
|
||||
# work_dir = "/tmp/status-page"
|
||||
# subprocess.run(["rm", "-rf", work_dir], check=True)
|
||||
# subprocess.run(["git", "clone", "--depth=1", REPO_URL, work_dir], check=True, capture_output=True)
|
||||
#
|
||||
# # Sync template files from ConfigMap mount
|
||||
# import shutil
|
||||
# for tpl in ["index.html", "CNAME"]:
|
||||
# src = os.path.join("/template", tpl)
|
||||
# dst = os.path.join(work_dir, tpl)
|
||||
# if os.path.exists(src):
|
||||
# shutil.copy2(src, dst)
|
||||
#
|
||||
# # Ensure .nojekyll exists
|
||||
# open(os.path.join(work_dir, ".nojekyll"), "a").close()
|
||||
#
|
||||
# with open(os.path.join(work_dir, "status.json"), "w") as f:
|
||||
# json.dump(status_data, f, indent=2)
|
||||
#
|
||||
# history_dir = os.path.join(work_dir, "history")
|
||||
# os.makedirs(history_dir, exist_ok=True)
|
||||
# today_file = os.path.join(history_dir, now.strftime("%Y-%m-%d") + ".json")
|
||||
# history = []
|
||||
# if os.path.exists(today_file):
|
||||
# with open(today_file) as f:
|
||||
# try:
|
||||
# history = json.load(f)
|
||||
# except json.JSONDecodeError:
|
||||
# history = []
|
||||
#
|
||||
# snapshot = {"timestamp": now.isoformat(), "monitors": {}}
|
||||
# for gname, gmonitors in groups.items():
|
||||
# for mon in gmonitors:
|
||||
# snapshot["monitors"][mon["name"]] = mon["status"]
|
||||
# history.append(snapshot)
|
||||
# with open(today_file, "w") as f:
|
||||
# json.dump(history, f)
|
||||
#
|
||||
# cutoff_date = (now - timedelta(days=30)).strftime("%Y-%m-%d")
|
||||
# for fname in os.listdir(history_dir):
|
||||
# if fname.endswith(".json") and fname < cutoff_date + ".json":
|
||||
# os.remove(os.path.join(history_dir, fname))
|
||||
# print(f" Deleted old history: {fname}")
|
||||
#
|
||||
# os.chdir(work_dir)
|
||||
# subprocess.run(["git", "config", "user.email", "status-bot@viktorbarzin.me"], check=True)
|
||||
# subprocess.run(["git", "config", "user.name", "Status Bot"], check=True)
|
||||
# subprocess.run(["git", "add", "-A"], check=True)
|
||||
#
|
||||
# result = subprocess.run(["git", "diff", "--cached", "--quiet"])
|
||||
# if result.returncode == 0:
|
||||
# print("No changes to push")
|
||||
# sys.exit(0)
|
||||
#
|
||||
# commit_msg = "status update " + now.strftime("%Y-%m-%d %H:%M UTC")
|
||||
# subprocess.run(["git", "commit", "-m", commit_msg], check=True)
|
||||
# push_result = subprocess.run(["git", "push"], capture_output=True, text=True)
|
||||
# if push_result.returncode != 0:
|
||||
# print(f"Push failed: {push_result.stderr}")
|
||||
# sys.exit(1)
|
||||
#
|
||||
# print(f"Successfully pushed status update at {now.isoformat()}")
|
||||
# PYEOF
|
||||
# EOT
|
||||
# ]
|
||||
# env {
|
||||
# name = "UPTIME_KUMA_PASSWORD"
|
||||
# value = data.vault_kv_secret_v2.viktor.data["uptime_kuma_admin_password"]
|
||||
# }
|
||||
# env {
|
||||
# name = "GITHUB_TOKEN"
|
||||
# value = data.vault_kv_secret_v2.viktor.data["github_pat"]
|
||||
# }
|
||||
# volume_mount {
|
||||
# name = "template"
|
||||
# mount_path = "/template"
|
||||
# read_only = true
|
||||
# }
|
||||
# resources {
|
||||
# requests = {
|
||||
# memory = "128Mi"
|
||||
# cpu = "10m"
|
||||
# }
|
||||
# limits = {
|
||||
# memory = "256Mi"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# volume {
|
||||
# name = "template"
|
||||
# config_map {
|
||||
# name = kubernetes_config_map_v1.status_page_template.metadata[0].name
|
||||
# }
|
||||
# }
|
||||
# dns_config {
|
||||
# option {
|
||||
# name = "ndots"
|
||||
# value = "2"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# lifecycle {
|
||||
# ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config] # KYVERNO_LIFECYCLE_V1
|
||||
# }
|
||||
# }
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue