UPTIME HISTORY
LAST 14 DAYS
(function(){
/* ==========================================================
CONFIGURATIE
========================================================== */
const STATUS_URL =
'https://status.oil4.nl/index.json';
/*
* Oil4 nieuwe server live:
* 14 september 2026.
*
* Na 30 dagen schakelt SINCE LAUNCH
* automatisch naar 30 DAYS.
*/
const LAUNCH_DATE =
new Date('2026-09-14T00:00:00+02:00');
const REFRESH_INTERVAL =
60000;
const WANTED_SERVICES = [
{
key:'web',
label:'Web'
},
{
key:'plesk',
label:'Plesk'
},
{
key:'imap',
label:'IMAP'
},
{
key:'pop3',
label:'POP3'
},
{
key:'smtp',
label:'SMTP'
}
];
/* ==========================================================
HELPERS
========================================================== */
function clamp(value,min,max){
return Math.min(
Math.max(value,min),
max
);
}
/*
* Better Stack availability kan afhankelijk
* van output bijvoorbeeld 1.0 of 100 zijn.
*/
function normalizeAvailability(value){
if(
value === null ||
value === undefined ||
value === ''
){
return null;
}
let number =
parseFloat(value);
if(isNaN(number)){
return null;
}
if(number >= 0 && number <= 1){
number *= 100;
}
return clamp(
number,
0,
100
);
}
function normalizeName(name){
return String(name || '')
.toLowerCase()
.replace(
/^oil4[\s\-_:]*/i,
''
)
.replace(
/[^a-z0-9]/g,
''
);
}
function isOperational(status){
const state =
String(status || '')
.toLowerCase();
return(
state === 'operational' ||
state === 'up' ||
state === 'resolved'
);
}
function uptimeClass(value){
if(
value === null ||
value === undefined ||
isNaN(value)
){
return 'status-na';
}
value =
Number(value);
if(value >= 99.999){
return 'status-perfect';
}
if(value >= 99.90){
return 'status-good';
}
if(value >= 99){
return 'status-warning';
}
return 'status-bad';
}
function percentage(value){
if(
value === null ||
value === undefined ||
isNaN(value)
){
return '--.--%';
}
return Number(value)
.toFixed(2)
+
'%';
}
function displayDate(date){
return date
.toLocaleDateString(
'nl-NL',
{
day:'2-digit',
month:'short'
}
)
.replace('.','')
.toUpperCase();
}
function localDayString(date){
const year =
date.getFullYear();
const month =
String(
date.getMonth() + 1
).padStart(2,'0');
const day =
String(
date.getDate()
).padStart(2,'0');
return(
year +
'-' +
month +
'-' +
day
);
}
/* ==========================================================
SINCE LAUNCH / 30 DAYS
========================================================== */
function updatePeriodLabel(){
const now =
new Date();
const elapsedDays =
(
now.getTime() -
LAUNCH_DATE.getTime()
)
/
86400000;
document.getElementById(
'oil4-period-label'
).textContent =
elapsedDays >= 30
? '30 DAYS'
: 'SINCE LAUNCH';
}
/* ==========================================================
GAUGE
========================================================== */
function setGauge(value){
const gauge =
document.getElementById(
'oil4-gauge-progress'
);
gauge.classList.remove(
'status-perfect',
'status-good',
'status-warning',
'status-bad'
);
if(
value === null ||
isNaN(value)
){
gauge.style.strokeDashoffset =
553;
return;
}
const p =
clamp(
Number(value),
0,
100
);
gauge.style.strokeDashoffset =
553 -
(
p /
100
)
*
553;
gauge.classList.add(
uptimeClass(p)
);
}
/* ==========================================================
BETTER STACK RESOURCES
========================================================== */
function getResources(json){
if(
!json ||
!Array.isArray(json.included)
){
return [];
}
return json.included
.filter(
function(item){
return(
item &&
item.type ===
'status_page_resource' &&
item.attributes
);
}
)
.map(
function(item){
const a =
item.attributes;
return{
id:item.id,
name:
a.public_name ||
'',
status:
a.status ||
'not_monitored',
availability:
normalizeAvailability(
a.availability
),
history:
Array.isArray(
a.status_history
)
? a.status_history
: []
};
}
);
}
/* ==========================================================
SERVICE MATCHING
========================================================== */
function findResource(
resources,
service
){
const wanted =
normalizeName(
service.key
);
/*
* Eerst exacte match.
*/
let resource =
resources.find(
function(item){
return(
normalizeName(
item.name
)
===
wanted
);
}
);
if(resource){
return resource;
}
/*
* Daarna bijvoorbeeld:
*
* Oil4 Web
* Oil4 - Web
* Oil4 Web Server
*/
resource =
resources.find(
function(item){
const name =
normalizeName(
item.name
);
return(
name.includes(
wanted
)
);
}
);
return resource || null;
}
/* ==========================================================
SERVICES RENDEREN
========================================================== */
function renderServices(resources){
const container =
document.getElementById(
'oil4-services-list'
);
container.innerHTML = '';
const matched = [];
WANTED_SERVICES.forEach(
function(service){
const resource =
findResource(
resources,
service
);
matched.push({
service:service,
resource:resource
});
const row =
document.createElement(
'div'
);
/*
* Alleen expliciete downtime/degraded
* wordt als probleem weergegeven.
*
* Niet gevonden = UNKNOWN,
* dus niet automatisch OFFLINE.
*/
let status =
resource
? resource.status
: 'unknown';
let availability =
resource
? resource.availability
: null;
let online =
resource
? isOperational(status)
: null;
let cssClass;
if(online === false){
cssClass =
'status-bad';
}
else if(
availability !== null
){
cssClass =
uptimeClass(
availability
);
}
else if(
online === true
){
cssClass =
'status-perfect';
}
else{
cssClass =
'status-na';
}
row.className =
'oil4-service-row ' +
cssClass;
let barWidth = 0;
if(online === false){
barWidth = 0;
}
else if(
availability !== null
){
barWidth =
availability;
}
else if(
online === true
){
barWidth = 100;
}
let label;
/*
* Gebruiker wilde rechts:
* ONLINE / OFFLINE.
*/
if(online === true){
label =
'ONLINE';
}
else if(online === false){
label =
'OFFLINE';
}
else{
label =
'---';
}
row.innerHTML =
'
' +
'' +
'' +
service.label +
'' +
'
' +
'
' +
'
';
container.appendChild(
row
);
}
);
return matched;
}
/* ==========================================================
DAG-UPTIME
========================================================== */
function calculateDayUptime(
matchedResources,
date
){
const day =
localDayString(date);
const now =
new Date();
const isToday =
localDayString(now)
===
day;
let elapsedSeconds;
if(isToday){
const start =
new Date(date);
start.setHours(
0,0,0,0
);
elapsedSeconds =
(
now.getTime() -
start.getTime()
)
/
1000;
}
else{
elapsedSeconds =
86400;
}
elapsedSeconds =
Math.max(
elapsedSeconds,
1
);
const uptimes = [];
matchedResources.forEach(
function(match){
const resource =
match.resource;
if(!resource){
return;
}
const record =
resource.history.find(
function(entry){
return(
String(
entry.day ||
''
)
.substring(0,10)
===
day
);
}
);
if(!record){
return;
}
const downtime =
parseFloat(
record.downtime_duration ||
0
);
/*
* Per service:
*
* uptime =
* actieve seconden - downtime.
*/
const uptime =
(
(
elapsedSeconds -
downtime
)
/
elapsedSeconds
)
*
100;
uptimes.push(
clamp(
uptime,
0,
100
)
);
}
);
if(!uptimes.length){
return null;
}
/*
* Voor totale infrastructuur nemen we
* de slechtste service van die dag.
*
* Daarmee wordt een storing niet
* weggemiddeld.
*/
return Math.min.apply(
null,
uptimes
);
}
/* ==========================================================
HISTORY
========================================================== */
function renderHistory(
matchedResources
){
const container =
document.getElementById(
'oil4-history'
);
container.innerHTML = '';
const today =
new Date();
today.setHours(
0,0,0,0
);
const launch =
new Date(
LAUNCH_DATE
);
launch.setHours(
0,0,0,0
);
const values = [];
for(
let offset = 13;
offset >= 0;
offset--
){
const date =
new Date(today);
date.setDate(
date.getDate() -
offset
);
const beforeLaunch =
date < launch;
let uptime = null;
if(!beforeLaunch){
uptime =
calculateDayUptime(
matchedResources,
date
);
}
/*
* Vandaag kan Better Stack vlak na
* livegang nog geen history-record
* hebben.
*
* Als alle gematchte services nu
* operational zijn, tonen we voor
* VANDAAG 100%.
*/
if(
offset === 0 &&
uptime === null
){
const realResources =
matchedResources
.map(
item =>
item.resource
)
.filter(Boolean);
if(
realResources.length &&
realResources.every(
resource =>
isOperational(
resource.status
)
)
){
uptime = 100;
}
}
if(
!beforeLaunch &&
uptime !== null
){
values.push({
date:date,
value:uptime
});
}
const element =
document.createElement(
'div'
);
let cssClass;
if(beforeLaunch){
cssClass =
'status-na';
}
else if(
uptime === null
){
cssClass =
'status-na';
}
else{
cssClass =
uptimeClass(
uptime
);
}
element.className =
'oil4-day ' +
cssClass;
if(offset === 0){
element.classList.add(
'today'
);
}
let fillHeight;
if(
beforeLaunch ||
uptime === null
){
fillHeight = 12;
}
else{
/*
* 99.9% moet visueel nog steeds
* bijna vol zijn.
*/
fillHeight =
Math.max(
15,
uptime
);
}
const uptimeText =
beforeLaunch
? 'N/A'
: uptime === null
? '--'
: percentage(
uptime
);
const label =
offset === 0
? 'VANDAAG'
: displayDate(date);
element.innerHTML =
'
' +
'
' +
uptimeText +
'
' +
'
' +
label +
'
';
container.appendChild(
element
);
}
return values;
}
/* ==========================================================
GROTE UPTIME
========================================================== */
function calculateOverall(
historyValues,
matchedResources
){
const now =
new Date();
const daysSinceLaunch =
(
now.getTime() -
LAUNCH_DATE.getTime()
)
/
86400000;
const maxDays =
daysSinceLaunch >= 30
? 30
: 30;
/*
* We gebruiken echte historische
* daggegevens wanneer beschikbaar.
*/
const validHistory =
historyValues
.slice(
-maxDays
);
if(validHistory.length){
let total = 0;
validHistory.forEach(
item => {
total +=
item.value;
}
);
return(
total /
validHistory.length
);
}
/*
* Dag 1:
* history kan nog leeg zijn.
*
* Dan availability gebruiken.
*/
const availabilities =
matchedResources
.map(
item =>
item.resource
? item.resource.availability
: null
)
.filter(
value =>
value !== null &&
!isNaN(value)
);
if(availabilities.length){
return Math.min.apply(
null,
availabilities
);
}
/*
* Zijn alle services operationeel,
* maar heeft Better Stack nog geen
* availability/history opgebouwd?
*
* Dan is vandaag vanaf launch 100%.
*/
const actualResources =
matchedResources
.map(
item =>
item.resource
)
.filter(Boolean);
if(
actualResources.length &&
actualResources.every(
resource =>
isOperational(
resource.status
)
)
){
return 100;
}
return null;
}
/* ==========================================================
MASTER STATUS
========================================================== */
function renderMasterStatus(
json,
matchedResources
){
const master =
document.getElementById(
'oil4-master-status'
);
const text =
document.getElementById(
'oil4-master-text'
);
const meter =
document.getElementById(
'oil4-meter-online'
);
/*
* Better Stack heeft zelf ook een
* aggregate_state op de statuspagina.
*/
const aggregateState =
json &&
json.data &&
json.data.attributes
? String(
json.data.attributes
.aggregate_state ||
''
).toLowerCase()
: '';
const resources =
matchedResources
.map(
item =>
item.resource
)
.filter(Boolean);
let operational;
if(aggregateState){
operational =
aggregateState ===
'operational';
}
else if(resources.length){
operational =
resources.every(
resource =>
isOperational(
resource.status
)
);
}
else{
operational = null;
}
master.classList.remove(
'loading',
'operational',
'issue'
);
meter.classList.remove(
'offline'
);
if(operational === true){
master.classList.add(
'operational'
);
text.textContent =
'ALL SYSTEMS OPERATIONAL';
meter.innerHTML =
' ONLINE';
}
else if(operational === false){
master.classList.add(
'issue'
);
text.textContent =
'SYSTEM ALERT';
meter.classList.add(
'offline'
);
meter.innerHTML =
' ALERT';
}
else{
text.textContent =
'STATUS UNKNOWN';
meter.innerHTML =
' CHECKING';
}
}
/* ==========================================================
LAST UPDATE
========================================================== */
function updateClock(){
const now =
new Date();
const date =
now.toLocaleDateString(
'nl-NL',
{
day:'2-digit',
month:'short',
year:'numeric'
}
);
const time =
now.toLocaleTimeString(
'nl-NL',
{
hour:'2-digit',
minute:'2-digit',
second:'2-digit'
}
);
document.getElementById(
'oil4-last-update'
).textContent =
'LAST UPDATE: ' +
date.toUpperCase() +
' · ' +
time;
}
/* ==========================================================
LOAD STATUS
========================================================== */
async function loadOil4Status(){
try{
const response =
await fetch(
STATUS_URL,
{
cache:'no-store'
}
);
if(!response.ok){
throw new Error(
'HTTP ' +
response.status
);
}
const json =
await response.json();
/*
* Juiste Better Stack structuur:
*
* included[]
* type=status_page_resource
* attributes.public_name
* attributes.status
* attributes.availability
* attributes.status_history
*/
const resources =
getResources(
json
);
const matchedResources =
renderServices(
resources
);
renderMasterStatus(
json,
matchedResources
);
const historyValues =
renderHistory(
matchedResources
);
const overall =
calculateOverall(
historyValues,
matchedResources
);
document.getElementById(
'oil4-main-uptime'
).textContent =
overall !== null
? percentage(overall)
: '--.--%';
setGauge(
overall
);
updatePeriodLabel();
updateClock();
}
catch(error){
console.error(
'Oil4 status error:',
error
);
document.getElementById(
'oil4-master-text'
).textContent =
'STATUS FEED OFFLINE';
const master =
document.getElementById(
'oil4-master-status'
);
master.classList.remove(
'operational'
);
master.classList.add(
'issue'
);
const meter =
document.getElementById(
'oil4-meter-online'
);
meter.classList.add(
'offline'
);
meter.innerHTML =
' FEED OFFLINE';
document.getElementById(
'oil4-main-uptime'
).textContent =
'--.--%';
setGauge(null);
}
}
/* ==========================================================
START
========================================================== */
updatePeriodLabel();
loadOil4Status();
setInterval(
loadOil4Status,
REFRESH_INTERVAL
);
})();