diff --git a/cronjobs/archive_cleanup.php b/cronjobs/archive_cleanup.php index 6f008d69..ebdd5b7e 100755 --- a/cronjobs/archive_cleanup.php +++ b/cronjobs/archive_cleanup.php @@ -25,5 +25,12 @@ require_once('shared.inc.php'); // If we don't keep archives, delete some now to release disk space if (!$share->purgeArchive()) { $log->logError("Failed to delete archived shares, not critical but should be checked!"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Failed to delete archived shares"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); } + +// Cron cleanup and monitoring +require_once('cron_end.inc.php'); ?> diff --git a/cronjobs/auto_payout.php b/cronjobs/auto_payout.php index 943a6cd7..c55cc913 100755 --- a/cronjobs/auto_payout.php +++ b/cronjobs/auto_payout.php @@ -24,12 +24,12 @@ require_once('shared.inc.php'); if ($bitcoin->can_connect() !== true) { $log->logFatal(" unable to connect to RPC server, exiting\n"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Unable to connect to RPC server"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } -// Mark this job as active -$setting->setValue('auto_payout_active', 1); - // Fetch all users with setup AP $users = $user->getAllAutoPayout(); @@ -80,7 +80,6 @@ if (! empty($users)) { $log->logDebug(" no user has configured their AP > 0\n"); } -// Mark this job as inactive -$setting->setValue('auto_payout_active', 0); - +// Cron cleanup and monitoring +require_once('cron_end.inc.php'); ?> diff --git a/cronjobs/blockupdate.php b/cronjobs/blockupdate.php index d257a69d..67d106b7 100755 --- a/cronjobs/blockupdate.php +++ b/cronjobs/blockupdate.php @@ -24,6 +24,9 @@ require_once('shared.inc.php'); if ( $bitcoin->can_connect() !== true ) { $log->logFatal("Failed to connect to RPC server\n"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Unable to connect to RPC server"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } @@ -51,3 +54,6 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $log->logError(' Failed to update block confirmations'); } } + +require_once('cron_end.inc.php'); +?> diff --git a/cronjobs/cron_end.inc.php b/cronjobs/cron_end.inc.php new file mode 100644 index 00000000..177e5296 --- /dev/null +++ b/cronjobs/cron_end.inc.php @@ -0,0 +1,28 @@ +setStatus($cron_name . "_message", "message", "OK"); +$monitoring->setStatus($cron_name . "_status", "okerror", 0); +$monitoring->setStatus($cron_name . "_runtime", "time", microtime(true) - $cron_start[$cron_name]); + +// Mark cron as running for monitoring +$monitoring->setStatus($cron_name . '_active', "yesno", 0); +?> diff --git a/cronjobs/findblock.php b/cronjobs/findblock.php index b98bbc3a..0336deea 100755 --- a/cronjobs/findblock.php +++ b/cronjobs/findblock.php @@ -32,6 +32,9 @@ if ( $bitcoin->can_connect() === true ){ $aTransactions = $bitcoin->query('listsinceblock', $strLastBlockHash); } else { $log->logFatal('Unable to conenct to RPC server backend'); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Unable to connect to RPC server"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } @@ -78,6 +81,9 @@ if (empty($aAllBlocks)) { $iAccountId = $user->getUserId($share->getUpstreamFinder()); } else { $log->logFatal('Unable to fetch blocks upstream share, aborted:' . $share->getError()); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Unable to fetch blocks " . $aBlock['height'] . " upstream share: " . $share->getError()); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit; } @@ -124,5 +130,6 @@ if (empty($aAllBlocks)) { } } } -?> +require_once('cron_end.inc.php'); +?> diff --git a/cronjobs/pplns_payout.php b/cronjobs/pplns_payout.php index 9657b394..6bb4234a 100755 --- a/cronjobs/pplns_payout.php +++ b/cronjobs/pplns_payout.php @@ -32,6 +32,9 @@ if ($config['payout_system'] != 'pplns') { $aAllBlocks = $block->getAllUnaccounted('ASC'); if (empty($aAllBlocks)) { $log->logDebug("No new unaccounted blocks found"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "No new unaccounted blocks"); + $monitoring->setStatus($cron_name . "_status", "okerror", 0); exit(0); } @@ -50,6 +53,9 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $iCurrentUpstreamId = $aBlock['share_id']; if (!is_numeric($iCurrentUpstreamId)) { $log->logFatal("Block " . $aBlock['height'] . " has no share_id associated with it, not going to continue"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Block " . $aBlock['height'] . " has no share_id associated with it"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } $iRoundShares = $share->getRoundShares($iPreviousShareId, $aBlock['share_id']); @@ -62,6 +68,9 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $aAccountShares = $share->getSharesForAccounts($aBlock['share_id'] - $pplns_target + 1, $aBlock['share_id']); if (empty($aAccountShares)) { $log->logFatal("No shares found for this block, aborted! Block Height : " . $aBlock['height']); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "No shares found for this block: " . $aBlock['height']); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } } else { @@ -71,6 +80,9 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $aAccountShares = $aRoundAccountShares; if (empty($aAccountShares)) { $log->logFatal("No shares found for this block, aborted! Block height: " . $aBlock['height']); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "No shares found for this block: " . $aBlock['height']); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } @@ -163,7 +175,13 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { // Mark this block as accounted for if (!$block->setAccounted($aBlock['id'])) { $log->logFatal("Failed to mark block as accounted! Aborting!"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Failed to mark block " . $aBlock['height'] . " as accounted"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } } } + +require_once('cron_end.inc.php'); +?> diff --git a/cronjobs/pps_payout.php b/cronjobs/pps_payout.php index 58b8dc83..bb48e62e 100755 --- a/cronjobs/pps_payout.php +++ b/cronjobs/pps_payout.php @@ -36,12 +36,15 @@ if ( $bitcoin->can_connect() === true ){ $dDifficulty = $dDifficulty['proof-of-work']; } else { $log->logFatal("Aborted: " . $bitcoin->can_connect() . "\n"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Unable to connect to RPC server"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } // Value per share calculation if ($config['reward_type'] != 'block') { -$pps_value = number_format(round($config['reward'] / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 12) ,12); + $pps_value = number_format(round($config['reward'] / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 12) ,12); } else { // Try to find the last block value and use that for future payouts, revert to fixed reward if none found if ($aLastBlock = $block->getLast()) { @@ -111,6 +114,9 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { $iLastBlockShare = @$aAllBlocks[$iIndex - 1]['share_id'] ? @$aAllBlocks[$iIndex - 1]['share_id'] : 0; if (!is_numeric($aBlock['share_id'])) { $log->logFatal("Block " . $aBlock['height'] . " has no share_id associated with it, not going to continue"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Block " . $aBlock['height'] . " has no share_id associated with it"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } // Per account statistics @@ -127,12 +133,20 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { // Delete shares if ($aBlock['share_id'] < $iLastShareId && !$share->deleteAccountedShares($aBlock['share_id'], $iLastBlockShare)) { $log->logFatal("Failed to delete accounted shares from " . $aBlock['share_id'] . " to " . $iLastBlockShare . ", aborting!"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Failed to delete accounted shares from " . $aBlock['share_id'] . " to " . $iLastBlockShare); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } // Mark this block as accounted for if (!$block->setAccounted($aBlock['id'])) { $log->logFatal("Failed to mark block as accounted! Aborting!"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Failed to mark block " . $aBlock['height'] . " as accounted"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } } + +require_once('cron.inc.php'); ?> diff --git a/cronjobs/proportional_payout.php b/cronjobs/proportional_payout.php index faca41ca..36a9a1fe 100755 --- a/cronjobs/proportional_payout.php +++ b/cronjobs/proportional_payout.php @@ -32,6 +32,9 @@ if ($config['payout_system'] != 'prop') { $aAllBlocks = $block->getAllUnaccounted('ASC'); if (empty($aAllBlocks)) { $log->logDebug('No new unaccounted blocks found in database'); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "No new unaccounted blocks"); + $monitoring->setStatus($cron_name . "_status", "okerror", 0); exit(0); } @@ -42,17 +45,15 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { if (!$aBlock['accounted']) { $iPreviousShareId = @$aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0; $iCurrentUpstreamId = $aBlock['share_id']; - if (!is_numeric($iCurrentUpstreamId)) { - $log->logFatal("Block " . $aBlock['height'] . " has no share_id associated with it, not going to continue."); - $log->logFatal("Please assign a valid share ID to this block to continue the payout process."); - exit(1); - } $aAccountShares = $share->getSharesForAccounts($iPreviousShareId, $aBlock['share_id']); $iRoundShares = $share->getRoundShares($iPreviousShareId, $aBlock['share_id']); $config['reward_type'] == 'block' ? $dReward = $aBlock['amount'] : $dReward = $config['reward']; if (empty($aAccountShares)) { $log->logFatal('No shares found for this block, aborted: ' . $aBlock['height']); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "No shares found for this block, aborted: " . $aBlock['height']); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } @@ -72,13 +73,13 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { // Verbose output of this users calculations $log->logInfo($aData['id'] . "\t" . - $aData['username'] . "\t" . - $aData['valid'] . "\t" . - $aData['invalid'] . "\t" . - $aData['percentage'] . "\t" . - $aData['payout'] . "\t" . - $aData['donation'] . "\t" . - $aData['fee']); + $aData['username'] . "\t" . + $aData['valid'] . "\t" . + $aData['invalid'] . "\t" . + $aData['percentage'] . "\t" . + $aData['payout'] . "\t" . + $aData['donation'] . "\t" . + $aData['fee']); // Update user share statistics if (!$statistics->updateShareStatistics($aData, $aBlock['id'])) @@ -102,10 +103,21 @@ foreach ($aAllBlocks as $iIndex => $aBlock) { // Delete all accounted shares if (!$share->deleteAccountedShares($iCurrentUpstreamId, $iPreviousShareId)) { $log->logFatal('Failed to delete accounted shares from ' . $iPreviousShareId . ' to ' . $iCurrentUpstreamId . ', aborted'); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Failed to delete accounted shares from " . $iPreviousShareId . " to " . $iCurrentUpstreamId); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } // Mark this block as accounted for - if (!$block->setAccounted($aBlock['id'])) + if (!$block->setAccounted($aBlock['id'])) { $log->logFatal('Failed to mark block as accounted! Aborted.'); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Failed to mark block " . $aBlock['height'] . " as accounted"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); + } } } + +require_once('cron_end.inc.php'); +?> diff --git a/cronjobs/shared.inc.php b/cronjobs/shared.inc.php index 9d3ae322..14bed27e 100644 --- a/cronjobs/shared.inc.php +++ b/cronjobs/shared.inc.php @@ -22,6 +22,13 @@ limitations under the License. // We need to find our include files so set this properly define("BASEPATH", "../public/"); + +/***************************************************** + * No need to change beyond this point * + *****************************************************/ +// Our cron name +$cron_name = basename($_SERVER['PHP_SELF'], '.php'); + // Our security check define("SECURITY", 1); @@ -32,6 +39,13 @@ require_once(BASEPATH . 'include/config/global.inc.php'); require_once(INCLUDE_DIR . '/autoloader.inc.php'); // Load 3rd party logging library for running crons -$log = new KLogger ( 'logs/' . basename($_SERVER['PHP_SELF'], '.php') . '.txt' , KLogger::DEBUG ); -$log->LogDebug('Starting ' . basename($_SERVER['PHP_SELF'], '.php')); +$log = new KLogger ( 'logs/' . $cron_name . '.txt' , KLogger::DEBUG ); +$log->LogDebug('Starting ' . $cron_name); + +// Load the start time for later runtime calculations for monitoring +$cron_start[$cron_name] = microtime(true); + +// Mark cron as running for monitoring +$log->logDebug('Marking cronjob as running for monitoring'); +$monitoring->setStatus($cron_name . '_active', 'yesno', 1); ?> diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index 68109049..281b39c6 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -24,6 +24,7 @@ require_once(INCLUDE_DIR . '/smarty.inc.php'); require_once(CLASS_DIR . '/base.class.php'); require_once(CLASS_DIR . '/block.class.php'); require_once(CLASS_DIR . '/setting.class.php'); +require_once(CLASS_DIR . '/monitoring.class.php'); require_once(CLASS_DIR . '/user.class.php'); require_once(CLASS_DIR . '/share.class.php'); require_once(CLASS_DIR . '/worker.class.php'); diff --git a/public/include/classes/monitoring.class.php b/public/include/classes/monitoring.class.php new file mode 100644 index 00000000..ff69007f --- /dev/null +++ b/public/include/classes/monitoring.class.php @@ -0,0 +1,49 @@ +debug = $debug; + $this->mysqli = $mysqli; + $this->table = 'monitoring'; + } + + /** + * Fetch a value from our table + * @param name string Setting name + * @return value string Value + **/ + public function getStatus($name) { + $query = $this->mysqli->prepare("SELECT * FROM $this->table WHERE name = ? LIMIT 1"); + if ($query && $query->bind_param('s', $name) && $query->execute() && $result = $query->get_result()) { + return $result->fetch_assoc(); + } else { + $this->debug->append("Failed to fetch variable $name from $this->table"); + return false; + } + return $value; + } + + /** + * Insert or update a setting + * @param name string Name of the variable + * @param value string Variable value + * @return bool + **/ + public function setStatus($name, $type, $value) { + $stmt = $this->mysqli->prepare(" + INSERT INTO $this->table (name, type, value) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE value = ? + "); + if ($stmt && $stmt->bind_param('ssss', $name, $type, $value, $value) && $stmt->execute()) + return true; + $this->debug->append("Failed to set $name to $value"); + return false; + } +} + +$monitoring = new Monitoring($debug, $mysqli); diff --git a/public/include/pages/account/edit.inc.php b/public/include/pages/account/edit.inc.php index 5548dd82..96fb432d 100644 --- a/public/include/pages/account/edit.inc.php +++ b/public/include/pages/account/edit.inc.php @@ -30,7 +30,8 @@ if ($user->isAuthenticated()) { if ($continue == true) { // Send balance to address, mind fee for transaction! try { - if ($setting->getValue('auto_payout_active') == 0) { + $auto_payout = $monitoring->getStatus('auto_payout_active'); + if ($auto_payout['value'] == 0) { $bitcoin->sendtoaddress($sCoinAddress, $dBalance); } else { $_SESSION['POPUP'][] = array('CONTENT' => 'Auto-payout active, please contact site support immidiately to revoke invalid transactions.', 'TYPE' => 'errormsg'); diff --git a/public/include/pages/admin/monitoring.inc.php b/public/include/pages/admin/monitoring.inc.php new file mode 100644 index 00000000..18db3949 --- /dev/null +++ b/public/include/pages/admin/monitoring.inc.php @@ -0,0 +1,70 @@ +isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { + header("HTTP/1.1 404 Page not found"); + die("404 Page not found"); +} + +// Fetch settings to propagate to template +$aCronStatus = array( + 'auto_payout' => array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('auto_payout_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('auto_payout_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('auto_payout_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('auto_payout_runtime') ) + ), + 'archive_cleanup' => array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('archive_cleanup_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('archive_cleanup_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('archive_cleanup_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('archive_cleanup_runtime') ) + ), + 'blockupdate' => array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('blockupdate_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('blockupdate_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('blockupdate_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('blockupdate_runtime') ) + ), + 'findblock' => array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('findblock_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('findblock_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('findblock_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('findblock_runtime') ) + ) +); +// Payout system specifics +switch ($config['payout_system']) { +case 'pplns': + $aCronStatus['pplns_payout'] = array ( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('pplns_payout_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pplns_payout_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('pplns_payout_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pplns_payout_runtime') ) + ); + break; +case 'pps': + $aCronStatus['pps_payout'] = array( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('pps_payout_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('pps_payout_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('pps_payout_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('pps_payout_runtime') ) + ); + break; +case 'prop': + $aCronStatus['proportional_payout'] = array( + array( 'NAME' => 'Exit Code', 'STATUS' => $monitoring->getStatus('proportional_payout_status') ), + array( 'NAME' => 'Last Message', 'STATUS' => $monitoring->getStatus('proportional_payout_message') ), + array( 'NAME' => 'Active', 'STATUS' => $monitoring->getStatus('proportional_payout_active') ), + array( 'NAME' => 'Runtime', 'STATUS' => $monitoring->getStatus('proportional_payout_runtime') ) + ); + break; +} +$smarty->assign("CRONSTATUS", $aCronStatus); + +// Tempalte specifics +$smarty->assign("CONTENT", "default.tpl"); +?> diff --git a/public/templates/mmcFE/admin/monitoring/default.tpl b/public/templates/mmcFE/admin/monitoring/default.tpl new file mode 100644 index 00000000..416d5fcf --- /dev/null +++ b/public/templates/mmcFE/admin/monitoring/default.tpl @@ -0,0 +1,39 @@ +{foreach $CRONSTATUS as $k=>$v} + {include file="global/block_header.tpl" BLOCK_HEADER="$k"} + + + + + + + {foreach $v as $event} + + + + + {/foreach} + +
Event NameStatus
{$event.NAME} + {if $event.STATUS.type == 'okerror'} + {if $event.STATUS.value == 0} + OK + {else} + ERROR + {/if} + {else if $event.STATUS.type == 'message'} + {$event.STATUS.value} + {else if $event.STATUS.type == 'yesno'} + {if $event.STATUS.value == 1} + Yes + {else} + No + {/if} + {else if $event.STATUS.type == 'time'} + {$event.STATUS.value|number_format:"2"} seconds + {else} + {$event.STATUS.value} + {/if} +
+ + {include file="global/block_footer.tpl"} +{/foreach} diff --git a/public/templates/mmcFE/global/navigation.tpl b/public/templates/mmcFE/global/navigation.tpl index 6cafd1f6..42c89e9a 100644 --- a/public/templates/mmcFE/global/navigation.tpl +++ b/public/templates/mmcFE/global/navigation.tpl @@ -13,6 +13,7 @@ {if $smarty.session.AUTHENTICATED|default:"0" == 1 && $GLOBAL.userdata.is_admin == 1}
  • Admin Panel