#!/usr/bin/php -q
<?PHP
/* Copyright 2005-2023, Lime Technology
 * Copyright 2012-2023, Bergware International.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version 2,
 * as published by the Free Software Foundation.
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 */
?>
<?
$docroot = '/usr/local/emhttp';
$varroot = '/var/local/emhttp';
$stamps  = '/var/tmp/stamps.ini';
$resync  = '/var/tmp/resync.ini';

require_once "$docroot/webGui/include/Helpers.php";
require_once "$docroot/webGui/include/publish.php";
extract(parse_plugin_cfg('dynamix',true));

// add translations
$_SERVER['REQUEST_URI'] = 'dashboard/main';
$login_locale = $display['locale'];
require_once "$docroot/webGui/include/Translations.php";

// remember current language
$locale_init = $locale;

function my_clock($time) {
  if (!$time) return _('less than a minute');
  $days = floor($time/1440);
  $hour = floor($time/60)%24;
  $mins = $time%60;
  return plus($days,'day',($hour|$mins)==0).plus($hour,'hour',$mins==0).plus($mins,'minute',true);
}
function find_day($D) {
  global $days;
  if ($days[0] == '*') return $D;
  foreach ($days as $d) if ($d >= $D) return $d;
  return $days[0];
}
function find_month($M) {
  global $months, $Y;
  if ($M > 12) {$M = 1; $Y++;}
  if ($months[0] == '*') return $M;
  foreach ($months as $m) if ($m >= $M) return $m;
  return $months[0];
}
function today($D) {
  global $days, $M, $Y;
  if ($days[0]=='*') return date('w',mktime(0,0,0,$M,$D,$Y));
  for ($d = $D; $d < $D+7; $d++) {
    $day = date('w',mktime(0,0,0,$M,$d,$Y));
    if (in_array($day,$days)) return $day;
  }
}
function next_day($D) {
  return find_day(($D+1)%7);
}
function last_day() {
  global $M, $Y;
  return date('t',mktime(0,0,0,$M,1,$Y));
}
function mkdate($D, $s) {
  global $M, $Y;
  if ($s > last_day()) {$s = 1; $M = find_month($M+1);}
  for ($d = $s; $d < $s+7; $d++) if ($D == date('w',mktime(0,0,0,$M,$d,$Y))) return $d;
}
function stage($i) {
  global $h, $m, $D, $M, $Y, $time, $now;
  if ($i < 0) {
    $d = $now ? $D : today(1);
    $s = $now ? date('j',$time) : 1;
    $D = mkdate($d, $s);
    $t = mktime($h,$m,0,$M,$D,$Y)-$time; // first day
    if ($t < 0) {
      $D = mkdate(next_day($d), $s+1);
      $t = mktime($h,$m,0,$M,$D,$Y)-$time; // next day
    }
    if ($t < 0) {
      $s += 7;
      if ($s > last_day()) {
        $s -= last_day();
        $M = find_month($M+1);
      }
      $D = mkdate(today($d), $s);
      $t = mktime($h,$m,0,$M,$D,$Y)-$time; // next week
    }
  } else {
    $d = $i ? ($now ? $D : today($i)) : today(last_day()-6);
    $s = $i ?: last_day()-6;
    $D = mkdate($d, $s);
    $t = mktime($h,$m,0,$M,$D,$Y)-$time; // first day
    if ($t < 0) {
      $D = mkdate(next_day($d), $s);
      $t = mktime($h,$m,0,$M,$D,$Y)-$time; // next day
    }
    if ($t < 0) {
      $M = find_month($M+1);
      $s = $i ?: last_day()-6;
      $D = mkdate(today($s), $s);
      $t = mktime($h,$m,0,$M,$D,$Y)-$time; // next month
    }
    if ($t < 0) {
      $Y++;
      $M = find_month(1);
      $s = $i ?: last_day()-6;
      $D = mkdate(today($s), $s);
      $t = mktime($h,$m,0,$M,$D,$Y)-$time; // next year
    }
  }
  return $t;
}
function normalize($type,$count) {
  $words = explode('_',$type);
  foreach ($words as &$word) $word = $word==strtoupper($word) ? $word : preg_replace(['/^(ct|cnt)$/','/^blk$/'],['count','block'],strtolower($word));
  return ucfirst(implode(' ',$words)).": ".str_replace('_',' ',strtolower($count))."\n";
}
function my_unit($value,$unit) {
  return ($unit=='F' ? round(9/5*$value+32) : $value)." $unit";
}
function active_disks($disk) {
  return substr($disk['status'],0,7)!='DISK_NP' && in_array($disk['type'],['Parity','Data']);
}
function device_name(&$disk) {
  switch ($disk['type']) {
    case 'Extra' :
    case 'Parity': $type = $disk['rotational'] ? 'disk' : 'nvme'; break;
    case 'Data'  :
    case 'Cache' : $type = $disk['rotational'] ? ($disk['luksState'] ? 'disk-encrypted' : 'disk') : 'nvme'; break;
  }
  $name = my_disk($disk['name']);
  [$p1,$p2] = my_explode(' ',$name);
  $name = _($p1).($p2?" $p2":"");
  return "<i class='icon-$type f14'></i> <a href=\"".htmlspecialchars("/Dashboard/Main/Settings/Device?name={$disk['name']}")."\" title=\"$name settings\">$name</a>";
}
function device_status(&$disk, &$error, &$warning) {
  global $var;
  if ($disk['type']!='Extra' && $var['fsState']=='Stopped') {
    $color = 'green'; $text = 'off-line';
  } else switch ($disk['color']) {
    case 'green-on'    : $color = 'green';  $text = 'active';     break;
    case 'green-blink' : $color = 'grey';   $text = 'standby';    break;
    case 'blue-on'     : $color = 'blue';   $text = 'unassigned'; break;
    case 'blue-blink'  : $color = 'grey';   $text = 'unassigned'; break;
    case 'yellow-on'   : $color = 'yellow'; $text =  $disk['type']=='Parity' ? 'invalid' : 'emulated';   $warning++; break;
    case 'yellow-blink': $color = 'grey';   $text =  $disk['type']=='Parity' ? 'invalid' : 'emulated';   $warning++; break;
    case 'red-on'      : $color = 'red';    $text = 'disabled';   $error++; break;
    case 'red-blink'   : $color = 'grey';   $text = 'disabled';   $error++; break;
    case 'red-off'     : $color = 'red';    $text = 'faulty';     $error++; break;
    case 'grey-off'    : $color = 'grey';   $text = 'no device';  break;
  }
  return "<i class='fa fa-circle orb $color-orb middle'></i>"._($text);
}
function device_temp(&$disk, &$red, &$orange) {
  global $display;
  $spin = strpos($disk['color'],'blink')===false;
  $temp = $disk['temp'];
  $hot = $disk['hotTemp'] ?? $display['hot'];
  $max = $disk['maxTemp'] ?? $display['max'];
  $top = $display['top'] ?? 120;
  $heat = false; $color = 'green';
  if (exceed($temp,$max,$top)) {
    $heat = 'fire'; $color = 'red'; $red++;
  } elseif (exceed($temp,$hot,$top)) {
    $heat = 'fire'; $color = 'orange'; $orange++;
  }
  return ($spin && $temp>0) ? "<span class='$color-text'>".my_unit($temp,$display['unit'])."</span>".($heat ? "<i class='fa fa-$heat $color-text heat'></i>" : "") : "*";
}
function device_smart(&$disk, &$fail, &$smart) {
  global $numbers,$saved;
  if (!$disk['device'] || strpos($disk['color'],'blink')) return "-";
  $failed = ['FAILED','NOK'];
  $name   = $disk['name'];
  $select = get_value($name,'smSelect',0);
  $level  = get_value($name,'smLevel',1);
  $events = explode('|',get_value($disk,'smEvents',$numbers));
  $title  = '';
  $thumb  = 'thumbs-o-up';
  $text   = _('healthy');
  $color  = 'green';
  $file   = "state/smart/$name";
  if (file_exists("$file") && exec("grep -Pom1 '^SMART.*: \K[A-Z]+' ".escapeshellarg($file)." |tr -d '\n' 2>/dev/null", $ssa) && in_array("$ssa",$failed)) {
    $title = _('SMART health-check failed')."\n"; $thumb = 'thumbs-o-down'; $color = 'red'; $text = 'fail'; $fail++;
  } else {
    if (empty($saved["smart"]["$name.ack"])) {
      exec("awk 'NR>7{print $1,$2,$4,$6,$9,$10}' ".escapeshellarg($file)." 2>/dev/null", $codes);
      foreach ($codes as $code) {
        if (!$code || !is_numeric($code[0])) continue;
        [$id,$class,$value,$thres,$when,$raw] = my_explode(' ',$code,7);
        $failing = strpos($when,'FAILING_NOW')!==false;
        if (!$failing && !in_array($id,$events)) continue;
        if ($failing || ($select ? $thres>0 && $value<=$thres*$level : $raw>0)) $title .= normalize($class,$failing?$when:$raw);
      }
      if ($title) {$thumb = 'thumbs-o-down'; $color = 'orange'; $text = _('error'); $smart++;} else $title = _('No errors reported')."\n";
    }
  }
  $title .= _('Click for context menu');
  return "<span id='smart-$name' name=Device class='fa fa-$thumb $color-text' style='margin-right:8px' onmouseover='this.style.cursor=\"pointer\"' title='$title'></span><span id='text-$name'>$text</span>";
}
function device_usage(&$disk, &$full, &$high) {
  global $display;
  $text = $display['text'];
  $used = ($disk['type']!='Parity' && $disk['type']!='Extra' && isset($disk['fsStatus']) && $disk['fsStatus']=='Mounted') ? ((isset($disk['fsSize']) ? round((1-$disk['fsFree']/$disk['fsSize'])*100):0).'%') : false;
  if ($used) {
    if ($text==2 || $text==21) {
      $load = substr($used,0,-1);
      $critical = $disk['critical'] ?: $display['critical'];
      $warning = $disk['warning'] ?: $display['warning'];
      if ($critical > 0 && $load >= $critical) {$class = 'redbar'; $full++;}
      elseif ($warning > 0 && $load >= $warning) {$class = 'orangebar'; $high++;}
      else $class = 'greenbar';
    }
    else
      $class = false;
    return $text%10==0 ? $used : "<span class='load'>$used</span><div class='usage-disk sys'><span style='width:$used'".($class?" class='$class'":"")."></span><span></span></div>";
  }
  else
    return $text%10==0 ? "-" : "<span class='load'>-</span><div class='usage-disk sys none'><span></span></div>";
}
function array_group($type,$pool=false) {
  global $disks,$error,$warning,$red,$orange,$fail,$smart,$full,$high;
  $echo = [];
  foreach ($disks as $disk) if ($disk['type']==$type && strpos($disk['status'],'DISK_NP')===false && (!$pool||$pool==prefix($disk['name']))) {
    $echo[] = "<tr class='updated'><td>";
    $echo[] = "<span class='w26'>".device_name($disk)."</span>";
    $echo[] = "<span class='w18'>".device_status($disk,$error,$warning)."</span>";
    $echo[] = "<span class='w18'>".device_temp($disk,$red,$orange)."</span>";
    $echo[] = "<span class='w18'>".device_smart($disk,$fail,$smart)."</span>";
    $echo[] = "<span class='w18'>".device_usage($disk,$full,$high)."</span>";
    $echo[] = "</td></tr>";
  }
  return implode('',$echo);
}
function extra_group() {
  global $devs,$error,$warning,$red,$orange,$fail,$smart,$full,$high;
  $echo = [];
  foreach ($devs as $disk) {
    $name = $disk['name'];
    $disk['type'] = "Extra";
    $disk['color'] = $disk['spundown']=="0" ? 'blue-on' : 'blue-blink';
    $echo[] = "<tr class='updated'><td>";
    $echo[] = "<span class='w26'>".device_name($disk)."</span>";
    $echo[] = "<span class='w18'>".device_status($disk,$error,$warning)."</span>";
    $echo[] = "<span class='w18'>".device_temp($disk,$red,$orange)."</span>";
    $echo[] = "<span class='w18'>".device_smart($disk,$fail,$smart)."</span>";
    $echo[] = "<span class='w18'>".device_usage($disk,$full,$high)."</span>";
    $echo[] = "</td></tr>";
  }
  return implode('',$echo);
}
function update_translation($locale) {
  global $docroot,$language;
  $language = [];
  if ($locale) {
    $text = "$docroot/languages/$locale/translations.txt";
    if (file_exists($text)) {
      $store = "$docroot/languages/$locale/translations.dot";
      if (!file_exists($store)) file_put_contents($store,serialize(parse_lang_file($text)));
      $language = unserialize(file_get_contents($store));
    }
    $text = "$docroot/languages/$locale/dashboard.txt";
    if (file_exists($text)) {
      $store = "$docroot/languages/$locale/dashboard.dot";
      if (!file_exists($store)) file_put_contents($store,serialize(parse_lang_file($text)));
      $language = array_merge($language,unserialize(file_get_contents($store)));
    }
  }
}
function create_sync($file) {
  return file_exists($file) ? explode(',',file_get_contents($file)) : [];
}
function print_error($error) {
  return sprintf(_('Finding **%s** error'.($error==1?'':'s')),$error?:'0');
}

while (true) {
  $var   = (array)parse_ini_file("$varroot/var.ini");
  $devs  = (array)parse_ini_file("$varroot/devs.ini",true);
  $disks = (array)parse_ini_file("$varroot/disks.ini",true);
  $saved = @(array)parse_ini_file("$varroot/monitor.ini",true);
  $echo  = [];
  $size  = $var['mdResyncSize'];
  $spot  = $var['mdResyncPos'];
  require "$docroot/webGui/include/CustomMerge.php";
  require "$docroot/webGui/include/Preselect.php";
  // check for language changes
  extract(parse_plugin_cfg('dynamix',true));
  if ($display['locale'] != $locale_init) {
    $locale_init = $display['locale'];
    update_translation($locale_init);
  }
  //array devices
  $error = $warning = $red = $orange = $fail = $smart = $full = $high = 0;
  $echo[0]  = array_group('Parity');
  $echo[0] .= array_group('Data');
  $echo[0] .= "\0".($error+$warning)."\0".($red+$orange)."\0".($fail+$smart)."\0".($full+$high);
  //pool devices
  $error = $warning = $red = $orange = $fail = $smart = $full = $high = 0;
  $echo[1] = '';
  foreach (pools_filter($disks) as $pool) if ($disks[$pool]['devices']) {
    $echo[1] .= array_group('Cache',$pool);
    $echo[1] .= "\0".($error+$warning)."\0".($red+$orange)."\0".($fail+$smart)."\0".($full+$high)."\r";
  }
  //unassigned devices
  $error = $warning = $red = $orange = $fail = $smart = $full = $high = 0;
  $echo[2]  = extra_group();
  $echo[2] .= "\0".($error+$warning)."\0".($red+$orange)."\0".($fail+$smart)."\0".($full+$high);

  // parity status
  $echo[3] = '';
  $disks = parity_filter($disks);
  $parity_slots = count($disks);
  $parity_disabled = $parity_invalid = 0;
  foreach ($disks as $disk) {
    if (strpos($disk['status'],"DISK_NP")===0) $parity_disabled++;
    elseif (strpos($disk['status'],"DISK_INVALID")===0) $parity_invalid++;
  }
  if ($spot) {
    $number = $display['number'];
    $action = preg_split('/\s+/',$var['mdResyncAction']);
    switch ($action[0]) {
      case "recon": $mode = $action[1]=='P' ? 'Parity-Sync' : 'Data-Rebuild'; break;
      case "check": $mode = count($action)>1 ? 'Parity-Check' : 'Read-Check'; break;
      case "clear": $mode = 'Disk-Clear'; break;
      default     : $mode = ''; break;
    }
    $echo[3] .= "<span class='orange'>"._($mode).' '._('in progress').'... '._('Completed').': '.number_format($spot/($size/100+1),1,$number[0],$number[1])." %.</span>";
  } else {
    if ($parity_slots==$parity_disabled) {
      $echo[3] .= "<span class='red'>"._('Parity disk'.($parity_slots==1?'':'s')." not present")."</span>";
    } elseif ($parity_slots > $parity_invalid) {
      if ($parity_invalid==0) {
        $echo[3] .= "<span class='green'>"._('Parity is valid')."</span>";
      } else {
        $echo[3] .= "<span class='orange'>"._('Parity is degraded').": $parity_invalid "._('invalid device'.($parity_invalid==1?'':'s'))."</span>";
      }
    } else {
      if (empty($var['mdInvalidDisk'])) {
        $echo[3] .= "<span class='red strong'>"._('Parity is invalid')."</span>";
      } else {
        $echo[3] .= "<span class='red strong'>"._('Data is invalid')."</span>";
      }
    }
  }

  // parity schedule
  [$delta,$bytes] = [$var['mdResyncDt'],$var['mdResyncDb']];
  $synced = create_sync($stamps);
  $sbSynced = array_shift($synced) ?: $var['sbSynced'];
  $sbUpdate = $delta ? $sbSynced : $var['sbUpdated'];
  if ($spot) {
    $echo[4] = sprintf(_('Current operation %s on **%s**'),($delta?_('started'):_('paused')),_(my_time($sbUpdate).day_count($sbUpdate),0));
    $echo[4].= "<br><i class='fa fa-fw fa-clock-o'></i> "._('Elapsed time').": "._(my_clock(floor((time()-$sbSynced)/60)),2);
    $echo[4].= "<br><i class='fa fa-fw fa-flag-checkered'></i> "._('Estimated finish').': '.($bytes ? _(my_clock(round(((($delta*(($size-$spot)/($bytes/100+1)))/100)/60),0)),2) : _('Unknown'));
    $echo[4].= "<br><i class='fa fa-fw fa-search'></i> ".print_error($var['sbSyncErrs']);
    $echo[4] .= "\0";
  } else {
    [$date,$duration,$speed,$status,$error,$action,$size] = last_parity_log();
    if ($var['sbSyncExit']!=0) {
      $echo[4] = sprintf(_('Last check incomplete on **%s**'),_(my_time($var['sbSynced2']).day_count($var['sbSynced2']),0));
      $echo[4].= "<br><i class='fa fa-fw fa-dot-circle-o'></i> "._('Error code').": ".my_error($var['sbSyncExit']);
      $echo[4].= "<br><i class='fa fa-fw fa-search'></i> ".print_error($var['sbSyncErrs']);
    } elseif ($var['sbSynced']==0) {
      if (!$date) {
        $echo[4] = _('Parity has not been checked yet');
      } elseif ($status==0) {
        $echo[4] = sprintf(_('Last checked on **%s**'),_(my_time($date).day_count($date),0));
        $echo[4].= "<br><i class='fa fa-fw fa-clock-o'></i> "._('Duration').": ".my_check($duration,$speed);
        $echo[4].= "<br><i class='fa fa-fw fa-search'></i> ".print_error($error);
      } else {
        $echo[4] = sprintf(_('Last check incomplete on **%s**'),_(my_time($date).day_count($date),0));
        $echo[4].= "<br><i class='fa fa-fw fa-dot-circle-o'></i> "._('Error code').": ".my_error($status);
        $echo[4].= "<br><i class='fa fa-fw fa-search'></i> ".print_error($error);
      }
    } elseif ($var['sbSynced2']==0) {
      if ($status==0) {
        $echo[4] = sprintf(_('Last checked on **%s**'),_(my_time($var['sbSynced']).day_count($var['sbSynced']),0));
        $echo[4].= "<br><i class='fa fa-fw fa-clock-o'></i> "._('Duration').": ".my_check($duration,$speed);
        $echo[4].= "<br><i class='fa fa-fw fa-search'></i> ".print_error($error);
      } else {
        $echo[4] = sprintf(_('Last check incomplete on **%s**'),_(my_time($var['sbSynced']).day_count($var['sbSynced']),0));
        $echo[4].= "<br><i class='fa fa-fw fa-dot-circle-o'></i> "._('Error code').": ".my_error($status);
        $echo[4].= "<br><i class='fa fa-fw fa-search'></i> ".print_error($error);
      }
    } else {
      $echo[4] = sprintf(_('Last check completed on **%s**'),_(my_time($var['sbSynced2']).day_count($var['sbSynced2']),0));
      $echo[4].= "<br><i class='fa fa-fw fa-clock-o'></i> "._('Duration').': '.my_check($duration,$speed);
      $echo[4].= "<br><i class='fa fa-fw fa-search'></i> ".print_error($var['sbSyncErrs']);
    }
    [$m,$h] = explode(' ', $parity['hour']);
    $time = time();
    $check = true;
    switch ($parity['mode']) {
    case 0: // check disabled
      $check = false;
      break;
    case 1: // daily check
      $t = mktime($h,$m,0)-$time;
      if ($t < 0) $t += 86400;
      break;
    case 2: // weekly check
      $t = $parity['day']-date('w',$time);
      if ($t < 0) $t += 7;
      $t = mktime($h,$m,0)+$t*86400-$time;
      if ($t < 0) $t += 86400*7;
      break;
    case 3: // monthly check
      $D = $parity['dotm'];
      $M = date('n',$time);
      $Y = date('Y',$time);
      $last = ($D == '28-31');
      if ($last) $D = last_day();
      $t = mktime($h,$m,0,$M,$D,$Y)-$time;
      if ($t < 0) {
        if ($M < 12) $M++; else {$M = 1; $Y++;}
        if ($last) $D = last_day();
        $t = mktime($h,$m,0,$M,$D,$Y)-$time;
      }
      break;
    case 4: // yearly check
      $D = $parity['dotm'];
      $M = $parity['month'];
      $Y = date('Y',$time);
      $last = ($D == '28-31');
      if ($last) $D = last_day();
      $t = mktime($h,$m,0,$M,$D,$Y)-$time;
      if ($t < 0) {
        $Y++;
        if ($last) $D = last_day();
        $t = mktime($h,$m,0,$M,$D,$Y)-$time;
      }
      break;
    case 5: // custom check
      $days = explode(',',$parity['day']);
      $months = explode(',',$parity['month']);
      $today = date('w',$time);
      $date = date('n',$time);
      $D = find_day($today);
      $M = find_month($date);
      $Y = date('Y',$time);
      $now = $M==$date;
      if ($M < $date) $Y++;
      switch ($parity['dotm']) {
      case '*' : $t = stage(-1); break;
      case 'W1': $t = stage(1); break;
      case 'W2': $t = stage(8); break;
      case 'W3': $t = stage(15); break;
      case 'W4': $t = stage(22); break;
      case 'WL': $t = stage(0); break;}
      break;
    }
    $echo[4] .= "\0";
    if ($check) {
      $frmt = $display['date'].($display['date']!='%c' ? ", {$display['time']}" : "");
      $echo[4] .= sprintf(_('Next check scheduled on **%s**'),_(my_date($frmt,$time+$t),0));
      $echo[4] .= "<br><i class='fa fa-fw fa-clock-o'></i> "._('Due in').": "._(my_clock(floor($t/60)),2);
    } else {
      $echo[4] .= _('Scheduled parity check is disabled');
    }
  }
  publish('update2', implode("\1",$echo));
  sleep(5);
}
?>
