Update remove crap managment

This commit is contained in:
DariusIII
2025-11-03 01:17:50 +01:00
parent 82c9758e0d
commit fa02ba0654
17 changed files with 421 additions and 68 deletions
+16 -7
View File
@@ -251,7 +251,7 @@ class Books
*
* @throws \Exception
*/
public function processBookReleases(): void
public function processBookReleases(string $groupID = '', string $guidChar = ''): void
{
$bookids = [];
if (ctype_digit((string) $this->bookreqids)) {
@@ -263,13 +263,22 @@ class Books
$total = \count($bookids);
if ($total > 0) {
foreach ($bookids as $i => $iValue) {
$query = Release::query()
->whereNull('bookinfo_id')
->whereIn('categories_id', [$iValue])
->orderByDesc('postdate')
->limit($this->bookqty);
if ($guidChar !== '') {
$query->where('leftguid', 'like', $guidChar.'%');
}
if ($groupID !== '') {
$query->where('groups_id', $groupID);
}
$this->processBookReleasesHelper(
Release::query()
->whereNull('bookinfo_id')
->whereIn('categories_id', [$iValue])
->orderByDesc('postdate')
->limit($this->bookqty)
->get(['searchname', 'id', 'categories_id']), $iValue
$query->get(['searchname', 'id', 'categories_id']), $iValue
);
}
}
+7 -2
View File
@@ -153,7 +153,12 @@ class TmuxRun extends Tmux
$log = $this->writelog($runVar['panes']['two'][2]);
shell_exec(
"tmux respawnp -t{$runVar['constants']['tmux_session']}:2.2 ' \
{$runVar['commands']['_phpn']} {$runVar['paths']['misc']}update/postprocess.php amazon true $log; date +\"{$this->_dateFormat}\"; {$runVar['commands']['_sleep']} {$runVar['settings']['post_timer_amazon']}' 2>&1 1> /dev/null"
{$runVar['commands']['_phpn']} artisan update:postprocess music true 2>&1 | tee -a $log; \
{$runVar['commands']['_phpn']} artisan update:postprocess book true 2>&1 | tee -a $log; \
{$runVar['commands']['_phpn']} artisan update:postprocess console true 2>&1 | tee -a $log; \
{$runVar['commands']['_phpn']} artisan update:postprocess games true 2>&1 | tee -a $log; \
{$runVar['commands']['_phpn']} artisan update:postprocess xxx true 2>&1 | tee -a $log; \
date +\"{$this->_dateFormat}\"; {$runVar['commands']['_sleep']} {$runVar['settings']['post_timer_amazon']}' 2>&1 1> /dev/null"
);
break;
case (int) $runVar['settings']['post_amazon'] === 1 && (int) $runVar['settings']['processbooks'] === 0
@@ -239,7 +244,7 @@ class TmuxRun extends Tmux
"tmux respawnp -t{$runVar['constants']['tmux_session']}:2.1 ' \
{$runVar['commands']['_php']} artisan multiprocessing:postprocess tv 2>&1 | tee -a $log; \
{$runVar['commands']['_php']} artisan multiprocessing:postprocess mov 2>&1 | tee -a $log; \
{$runVar['commands']['_php']} {$runVar['paths']['misc']}update/postprocess.php anime true $log; \
{$runVar['commands']['_php']} artisan multiprocessing:postprocess anime 2>&1 | tee -a $log; \ $log; \
{$runVar['commands']['_php']} {$runVar['paths']['misc']}testing/PostProc/check_covers.php true $log; \
date +\"{$this->_dateFormat}\"; {$runVar['commands']['_sleep']} {$runVar['settings']['post_timer_non']}' 2>&1 1> /dev/null"
);
+5 -1
View File
@@ -199,13 +199,17 @@ class Forking
break;
case 'postProcess_ama':
$this->processSingle();
$this->postProcessRunner->processBooks();
break;
case 'postProcess_add':
$this->postProcessRunner->processAdditional();
break;
case 'postProcess_ani':
$this->postProcessRunner->processAnime();
break;
case 'postProcess_mov':
$renamedOnly = (isset($this->workTypeOptions[0]) && $this->workTypeOptions[0] === true);
$this->postProcessRunner->processMovies($renamedOnly);
@@ -212,4 +212,68 @@ class PostProcessRunner extends BaseRunner
return count(DB::select($checkSql)) > 0;
}
public function processAnime(): void
{
if ((int) Settings::settingValue('lookupanidb') <= 0) {
$this->headerNone();
return;
}
$checkSql = '
SELECT id
FROM releases
WHERE categories_id = 5070
AND anidbid IS NULL
LIMIT 1';
if (count(DB::select($checkSql)) === 0) {
$this->headerNone();
return;
}
$sql = '
SELECT DISTINCT LEFT(leftguid, 1) AS id
FROM releases
WHERE categories_id = 5070
AND anidbid IS NULL
LIMIT 16';
$queue = DB::select($sql);
$maxProcesses = (int) Settings::settingValue('postthreadsnon');
$this->runPostProcess($queue, $maxProcesses, 'anime', 'anime postprocessing');
}
public function processBooks(): void
{
if ((int) Settings::settingValue('lookupbooks') <= 0) {
$this->headerNone();
return;
}
$checkSql = '
SELECT id
FROM releases
WHERE categories_id BETWEEN 7000 AND 7999
AND bookinfo_id IS NULL
LIMIT 1';
if (count(DB::select($checkSql)) === 0) {
$this->headerNone();
return;
}
$sql = '
SELECT DISTINCT LEFT(leftguid, 1) AS id
FROM releases
WHERE categories_id BETWEEN 7000 AND 7999
AND bookinfo_id IS NULL
LIMIT 16';
$queue = DB::select($sql);
$maxProcesses = (int) Settings::settingValue('postthreadsnon');
$this->runPostProcess($queue, $maxProcesses, 'books', 'books postprocessing');
}
}
+8 -4
View File
@@ -112,23 +112,27 @@ class PostProcess
/**
* Lookup anidb if enabled.
*
* @param string $groupID (Optional) ID of a group to work on.
* @param string $guidChar (Optional) First letter of a release GUID to use to get work.
*
* @throws \Exception
*/
public function processAnime(): void
public function processAnime(string $groupID = '', string $guidChar = ''): void
{
$this->animeProcessor->process();
$this->animeProcessor->process($groupID, $guidChar);
}
/**
* Process books using amazon.com.
*
* @param string $groupID (Optional) ID of a group to work on.
* @param string $guidChar (Optional) First letter of a release GUID to use to get work.
*
* @throws \Exception
*/
public function processBooks(): void
public function processBooks(string $groupID = '', string $guidChar = ''): void
{
$this->booksProcessor->process();
$this->booksProcessor->process($groupID, $guidChar);
}
/**
+16 -4
View File
@@ -56,14 +56,26 @@ class AniDB
/**
* Queues anime releases for processing.
*
* @param string $groupID (Optional) ID of a group to work on.
* @param string $guidChar (Optional) First letter of a release GUID to use to get work.
*
* @throws \Exception
*/
public function processAnimeReleases(): void
public function processAnimeReleases(string $groupID = '', string $guidChar = ''): void
{
$results = Release::query()
$query = Release::query()
->whereNull('anidbid')
->where('categories_id', Category::TV_ANIME)
->orderByDesc('postdate')
->where('categories_id', Category::TV_ANIME);
if ($guidChar !== '') {
$query->where('leftguid', 'like', $guidChar.'%');
}
if ($groupID !== '') {
$query->where('groups_id', $groupID);
}
$results = $query->orderByDesc('postdate')
->limit($this->aniqty)
->get();
+10 -2
View File
@@ -18,7 +18,7 @@ class PostProcessGuid extends Command
* @var string
*/
protected $signature = 'postprocess:guid
{type : Type: additional, nfo, movie, or tv}
{type : Type: additional, nfo, movie, tv, anime, or books}
{guid : First character of release guid (a-f, 0-9)}
{renamed? : For movie/tv: process renamed only (optional)}';
@@ -70,8 +70,16 @@ class PostProcessGuid extends Command
(new PostProcess)->processTv('', $guid, $renamed);
break;
case 'anime':
(new PostProcess)->processAnime('', $guid);
break;
case 'books':
(new PostProcess)->processBooks('', $guid);
break;
default:
$this->error('Invalid type. Must be: additional, nfo, movie, or tv.');
$this->error('Invalid type. Must be: additional, nfo, movie, tv, anime, or books.');
return self::FAILURE;
}
+5 -4
View File
@@ -13,7 +13,7 @@ class ProcessPostProcess extends Command
* @var string
*/
protected $signature = 'multiprocessing:postprocess
{type : Type: ama, add, mov, nfo, sha, or tv}
{type : Type: ama, add, ani, mov, nfo, sha, or tv}
{renamed=false : For mov/tv: only post-process renamed releases (true/false)}';
/**
@@ -31,11 +31,12 @@ class ProcessPostProcess extends Command
$type = $this->argument('type');
$renamed = $this->argument('renamed');
if (! \in_array($type, ['ama', 'add', 'mov', 'nfo', 'sha', 'tv'], true)) {
$this->error('Type must be one of: ama, add, mov, nfo, sha, tv');
if (! \in_array($type, ['ama', 'add', 'ani', 'mov', 'nfo', 'sha', 'tv'], true)) {
$this->error('Type must be one of: ama, add, ani, mov, nfo, sha, tv');
$this->line('');
$this->line('ama => Do amazon processing (no multiprocessing due to API restrictions)');
$this->line('ama => Do amazon/books processing');
$this->line('add => Do additional (rar|zip) processing');
$this->line('ani => Do anime processing');
$this->line('mov => Do movie processing');
$this->line('nfo => Do NFO processing');
$this->line('sha => Do sharing processing (no multiprocessing)');
@@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use Blacklight\ReleaseRemover;
use Illuminate\Console\Command;
class RemoveCrapReleases extends Command
{
protected $signature = 'releases:remove-crap
{--type= : Type of crap to remove}
{--time=full : Time limit in hours or full}
{--blacklist-id= : Specific blacklist ID}
{--delete : Actually delete releases}';
protected $description = 'Remove crap releases based on various criteria';
public function __construct()
{
parent::__construct();
}
public function handle(): int
{
$type = $this->option('type') ?? '';
$time = $this->option('time') ?? 'full';
$blacklistId = $this->option('blacklist-id') ?? '';
$delete = $this->option('delete');
if (! $delete) {
$this->warn('Running in DRY-RUN mode. Use --delete to actually remove releases.');
}
try {
$remover = new ReleaseRemover;
$result = $remover->removeCrap($delete, $time, $type, $blacklistId);
if ($result === true) {
return self::SUCCESS;
}
$this->error($result);
return self::FAILURE;
} catch (\Exception $e) {
$this->error($e->getMessage());
return self::FAILURE;
}
}
}
@@ -20,7 +20,17 @@ class AdminTmuxController extends BasePageController
switch ($action) {
case 'submit':
Settings::settingsUpdate($request->all());
$data = $request->all();
// Handle fix_crap checkbox array - convert to comma-separated string
if (isset($data['fix_crap']) && is_array($data['fix_crap'])) {
$data['fix_crap'] = implode(',', $data['fix_crap']);
} elseif (! isset($data['fix_crap'])) {
// If no checkboxes selected, save empty string
$data['fix_crap'] = '';
}
Settings::settingsUpdate($data);
return redirect()->to('admin/tmux-edit')->with('success', 'Tmux settings updated successfully');
+2 -2
View File
@@ -14,10 +14,10 @@ class AnimeProcessor
$this->echooutput = $echooutput;
}
public function process(): void
public function process(string $groupID = '', string $guidChar = ''): void
{
if ((int) Settings::settingValue('lookupanidb') !== 0) {
(new AniDB)->processAnimeReleases();
(new AniDB)->processAnimeReleases($groupID, $guidChar);
}
}
}
+2 -2
View File
@@ -14,10 +14,10 @@ class BooksProcessor
$this->echooutput = $echooutput;
}
public function process(): void
public function process(string $groupID = '', string $guidChar = ''): void
{
if ((int) Settings::settingValue('lookupbooks') !== 0) {
(new Books)->processBookReleases();
(new Books)->processBookReleases($groupID, $guidChar);
}
}
}
+5 -1
View File
@@ -87,8 +87,12 @@ class TmuxPaneManager
{
$killFlag = $kill ? '-k' : '';
// Escape the command for tmux by replacing double quotes with escaped quotes
$escapedCommand = str_replace('"', '\\"', $command);
$escapedCommand = str_replace('$', '\\$', $escapedCommand);
$result = Process::timeout(10)->run(
"tmux respawnp {$killFlag} -t {$this->sessionName}:{$target} '{$command}'"
"tmux respawnp {$killFlag} -t {$this->sessionName}:{$target} \"{$escapedCommand}\""
);
return $result->successful();
+104 -19
View File
@@ -71,7 +71,7 @@ class TmuxTaskRunner
// Add sleep timer at the end if specified
if (isset($options['sleep'])) {
$sleepCommand = $this->buildSleepCommand($options['sleep']);
$parts[] = "date +'%Y-%m-%d %T'";
$parts[] = 'date +"%Y-%m-%d %T"';
$parts[] = $sleepCommand;
}
@@ -334,16 +334,12 @@ class TmuxTaskRunner
*/
protected function runMainSequential(array $runVar): bool
{
// Full sequential mode - runs sequential script
// Full sequential mode - runs group:update-all for each group
$pane = '0.1';
$script = base_path('misc/update/nix/tmux/bin/sequential.php');
if (! file_exists($script)) {
return $this->disablePane($pane, 'Sequential', 'script not found');
}
$niceness = Settings::settingValue('niceness') ?? 2;
$command = "nice -n{$niceness} php {$script}";
$artisan = base_path('artisan');
$command = "nice -n{$niceness} php {$artisan} group:update-all";
$command = $this->buildCommand($command, ['log_pane' => 'sequential']);
return $this->paneManager->respawnPane($pane, $command);
@@ -387,26 +383,115 @@ class TmuxTaskRunner
*/
protected function runRemoveCrapTask(array $runVar): bool
{
$enabled = (int) ($runVar['settings']['fix_crap'] ?? 0);
$option = $runVar['settings']['fix_crap_opt'] ?? 'Disabled';
$pane = '1.1';
if ($enabled !== 1) {
// Handle disabled state
if ($option === 'Disabled' || $option === 0 || $option === '0') {
return $this->disablePane($pane, 'Remove Crap', 'disabled in settings');
}
$option = (int) ($runVar['settings']['fix_crap_opt'] ?? 1);
$script = base_path('misc/update/nix/tmux/bin/removecrap.php');
$niceness = Settings::settingValue('niceness') ?? 2;
$artisan = base_path('artisan');
$sleep = (int) ($runVar['settings']['crap_timer'] ?? 300);
if (! file_exists($script)) {
return $this->disablePane($pane, 'Remove Crap', 'script not found');
// Handle 'All' mode - run all types with 2 hour time limit
if ($option === 'All') {
$command = "nice -n{$niceness} php {$artisan} releases:remove-crap --time=2 --delete";
$command = $this->buildCommand($command, ['log_pane' => 'removecrap', 'sleep' => $sleep]);
// Wrap in while loop to keep pane alive
$loopCommand = "while true; do {$command}; done";
return $this->paneManager->respawnPane($pane, $loopCommand);
}
$niceness = Settings::settingValue('niceness') ?? 2;
$command = "nice -n{$niceness} php {$script} {$option}";
$sleep = (int) ($runVar['settings']['crap_timer'] ?? 300);
$command = $this->buildCommand($command, ['log_pane' => 'removecrap', 'sleep' => $sleep]);
// Handle 'Custom' mode - cycle through selected types
if ($option === 'Custom') {
$selectedTypes = $runVar['settings']['fix_crap'] ?? '';
return $this->paneManager->respawnPane($pane, $command);
if (empty($selectedTypes)) {
return $this->disablePane($pane, 'Remove Crap', 'no crap types selected');
}
$types = is_array($selectedTypes) ? $selectedTypes : explode(',', $selectedTypes);
$types = array_filter($types); // Remove empty values
if (empty($types)) {
return $this->disablePane($pane, 'Remove Crap', 'no crap types selected');
}
// Get the next type to process
$stateFile = storage_path('tmux/removecrap_state.json');
$state = $this->loadCrapState($stateFile);
// Determine current type index
$currentIndex = $state['current_index'] ?? 0;
$isFirstRun = $state['first_run'] ?? true;
// Validate index
if ($currentIndex >= count($types)) {
$currentIndex = 0;
$isFirstRun = false;
}
$currentType = $types[$currentIndex];
// Determine time limit: full on first run, 4 hours otherwise
$time = $isFirstRun ? 'full' : '4';
// Build and run command
$command = "nice -n{$niceness} php {$artisan} releases:remove-crap --type={$currentType} --time={$time} --delete";
$command = $this->buildCommand($command, ['log_pane' => 'removecrap', 'sleep' => $sleep]);
// Update state for next run
$nextIndex = $currentIndex + 1;
if ($nextIndex >= count($types)) {
$nextIndex = 0;
$isFirstRun = false;
}
$this->saveCrapState($stateFile, [
'current_index' => $nextIndex,
'first_run' => $isFirstRun,
'types' => $types,
]);
// Wrap in while loop to keep pane alive
$loopCommand = "while true; do {$command}; done";
return $this->paneManager->respawnPane($pane, $loopCommand);
}
// Default fallback - disabled
return $this->disablePane($pane, 'Remove Crap', 'invalid configuration');
}
/**
* Load crap removal state
*/
protected function loadCrapState(string $file): array
{
if (! file_exists($file)) {
return ['current_index' => 0, 'first_run' => true];
}
$content = file_get_contents($file);
$state = json_decode($content, true);
return $state ?: ['current_index' => 0, 'first_run' => true];
}
/**
* Save crap removal state
*/
protected function saveCrapState(string $file, array $state): void
{
$dir = dirname($file);
if (! is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($file, json_encode($state, JSON_PRETTY_PRINT));
}
/**
+19
View File
@@ -627,3 +627,22 @@ code.api-token::-webkit-scrollbar-thumb:hover {
}
}
/* Tmux Edit - Remove Crap Types Container Styles */
#crap_types_container {
transition: opacity 0.3s ease, display 0.3s ease;
}
/* Checkbox hover effects for crap types */
.hover\:bg-gray-100:hover {
transition: background-color 0.2s ease;
}
.dark .hover\:bg-gray-700:hover {
transition: background-color 0.2s ease;
}
/* Form checkbox focus ring */
.form-checkbox:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;
}
+63 -13
View File
@@ -383,7 +383,7 @@ window.showConfirm = function(options) {
const messageText = document.getElementById('confirmationModalMessage');
const confirmText = document.getElementById('confirmationModalConfirmText');
const cancelText = document.getElementById('confirmationModalCancelText');
if (titleText) titleText.textContent = config.title;
if (messageText) messageText.textContent = config.message;
if (confirmText) confirmText.textContent = config.confirmText;
@@ -2649,7 +2649,7 @@ function initMoviesLayoutToggle() {
applyLayout(currentLayout); // Apply layout anyway
return;
}
fetch('/movies/update-layout', {
method: 'POST',
headers: {
@@ -2771,29 +2771,29 @@ function initAdminDashboardCharts() {
}, 100);
return;
}
initializeAllDashboardCharts();
}
function initializeAllDashboardCharts() {
// Check if any chart canvas elements exist
const hasUserStatsCharts = document.getElementById('downloadsChart') ||
const hasUserStatsCharts = document.getElementById('downloadsChart') ||
document.getElementById('apiHitsChart') ||
document.getElementById('downloadsMinuteChart') ||
document.getElementById('apiHitsMinuteChart');
const hasSystemCharts = document.getElementById('cpuHistory24hChart') ||
document.getElementById('ramHistory24hChart') ||
document.getElementById('cpuHistory30dChart') ||
document.getElementById('ramHistory30dChart');
if (!hasUserStatsCharts && !hasSystemCharts) {
return; // Not on admin dashboard or charts not present
}
// Initialize user statistics charts
initializeUserStatCharts();
// Initialize system metrics charts
initializeSystemMetricsCharts();
}
@@ -3477,8 +3477,8 @@ function initAdminRegexFormValidation() {
// and have matching delimiters
if (value.length > 0) {
const firstChar = value[0];
const delimiters = ['/', '#', '~', '%', '@', '!'];
const delimiters = ['/', '#', '~', '%', '@', '!',];
if (delimiters.includes(firstChar)) {
// Check if regex has matching closing delimiter
let delimiterCount = 0;
@@ -3531,8 +3531,8 @@ function initAdminRegexFormValidation() {
const value = this.value.trim();
if (value) {
const firstChar = value[0];
const delimiters = ['/', '#', '~', '%', '@', '!'];
const delimiters = ['/', '#', '~', '%', '@', '!',];
if (delimiters.includes(firstChar)) {
let delimiterCount = 0;
for (let i = 1; i < value.length; i++) {
@@ -3541,7 +3541,7 @@ function initAdminRegexFormValidation() {
break;
}
}
if (delimiterCount === 0) {
this.classList.add('border-red-500');
this.classList.remove('border-green-500');
@@ -3564,7 +3564,7 @@ function initAdminTmuxSelectColors() {
// This function is for future tmux color selection functionality
// Currently, no color selection is needed in tmux settings
// If color pickers are added to tmux-edit page in the future, add them here
// Check if we're on a page that might have color inputs
const colorInputs = document.querySelectorAll('input[type="color"], select[data-color-select]');
if (colorInputs.length > 0) {
@@ -4248,3 +4248,53 @@ function initFlashMessages() {
initFlashMessages();
}
})();
// Tmux Edit - Remove Crap Releases Toggle
function toggleCrapTypes(value) {
const container = document.getElementById('crap_types_container');
if (container) {
if (value === 'Custom') {
container.style.display = 'block';
// Add a slight animation effect
setTimeout(() => {
container.style.opacity = '1';
}, 10);
} else {
container.style.opacity = '0.5';
setTimeout(() => {
container.style.display = 'none';
container.style.opacity = '1';
}, 200);
}
}
}
// Initialize tmux edit page functionality
function initTmuxEdit() {
// Initialize crap types toggle on page load
const checkedRadio = document.querySelector('input[name="fix_crap_opt"]:checked');
if (checkedRadio) {
toggleCrapTypes(checkedRadio.value);
}
// Add event listeners to all radio buttons
document.querySelectorAll('input[name="fix_crap_opt"]').forEach(function(radio) {
radio.addEventListener('change', function() {
toggleCrapTypes(this.value);
});
});
}
// Initialize on DOMContentLoaded if on tmux-edit page
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
if (document.getElementById('crap_types_container')) {
initTmuxEdit();
}
});
} else {
if (document.getElementById('crap_types_container')) {
initTmuxEdit();
}
}
+33 -6
View File
@@ -304,15 +304,41 @@
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Remove Crap Releases</h2>
<div class="space-y-4">
<x-form.group label="Remove Crap Releases" for="fix_crap_opt" help="Remove passworded and other junk releases.">
<x-select id="fix_crap_opt" name="fix_crap_opt" class="w-full">
@foreach($yesno_ids as $index => $val)
<option value="{{ $val }}" {{ ($site['fix_crap_opt'] ?? '') == $val ? 'selected' : '' }}>
{{ $yesno_names[$index] }}
</option>
<div class="space-y-2">
@foreach($fix_crap_radio_ids as $index => $val)
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio"
id="fix_crap_opt_{{ $val }}"
name="fix_crap_opt"
value="{{ $val }}"
{{ ($site['fix_crap_opt'] ?? 'Disabled') == $val ? 'checked' : '' }}
class="form-radio h-4 w-4 text-blue-600 dark:bg-gray-700 dark:border-gray-600">
<span class="text-sm text-gray-700 dark:text-gray-300">{{ $fix_crap_radio_names[$index] }}</span>
</label>
@endforeach
</x-select>
</div>
</x-form.group>
<div id="crap_types_container">
<x-form.group label="Select Crap Types" for="fix_crap_types" help="Choose which types of crap releases to remove.">
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border-2 border-blue-200 dark:border-blue-700">
@php
$selectedTypes = explode(',', $site['fix_crap'] ?? '');
@endphp
@foreach($fix_crap_check_ids as $index => $val)
<label class="flex items-center space-x-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded transition-colors">
<input type="checkbox"
name="fix_crap[]"
value="{{ $val }}"
{{ in_array($val, $selectedTypes) ? 'checked' : '' }}
class="form-checkbox h-4 w-4 text-blue-600 rounded dark:bg-gray-700 dark:border-gray-600">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ ucfirst($fix_crap_check_names[$index]) }}</span>
</label>
@endforeach
</div>
</x-form.group>
</div>
<x-form.group label="Remove Crap Sleep Timer" for="crap_timer">
<div class="flex gap-2">
<x-input id="crap_timer" name="crap_timer" type="number" value="{{ $site['crap_timer'] ?? 300 }}" class="flex-1" />
@@ -322,6 +348,7 @@
</div>
</div>
<!-- Console & Monitoring Tools -->
<div class="border-b border-gray-200 dark:border-gray-700 pb-6">
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Console & Monitoring Tools</h2>