Added system polling

This commit is contained in:
Deon George 2023-07-26 19:44:07 +10:00
parent c23b5ebfc2
commit 4e44e2e266
19 changed files with 733 additions and 88 deletions

View File

@ -28,7 +28,9 @@ abstract class Protocol
// Our sessions Types
public const SESSION_AUTO = 0;
/** @deprecate Use mailers:class */
public const SESSION_EMSI = 1;
/** @deprecate Use mailers:class */
public const SESSION_BINKP = 2;
public const SESSION_ZMODEM = 3;
@ -447,6 +449,7 @@ abstract class Protocol
$slo->sessiontype = $type;
$slo->sessiontime = $this->node->session_time;
$slo->result = ($rc & self::S_MASK);
$slo->originate = $this->originate;
$so->logs()->save($slo);
}

View File

@ -1420,8 +1420,6 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
$trys++;
}
dump(['ls_rxHdr'=>hex_dump(join('',$this->ls_rxHdr))]);
switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout))) {
/* Send ZRINIT again */
case self::ZRQINIT:

View File

@ -34,14 +34,14 @@ class CommBinkpSend extends Command
*/
public function handle(): void
{
Log::info('CBS:- Call BINKP send');
$mo = Mailer::where('name',self::ID)->singleOrFail();
$ao = Address::findFTN($this->argument('ftn'));
if (! $ao)
throw new ModelNotFoundException('Unknown node: '.$this->argument('ftn'));
Job::dispatchSync($ao,$mo);
Log::info(sprintf('CBS:- Call BINKP send for %s',$ao->ftn));
$mo = Mailer::where('name',self::ID)->singleOrFail();
Job::dispatch($ao,$mo);
}
}

View File

@ -34,14 +34,14 @@ class CommEMSISend extends Command
*/
public function handle(): void
{
Log::info('CES:- Call EMSI send');
$mo = Mailer::where('name',self::ID)->singleOrFail();
$ao = Address::findFTN($this->argument('ftn'));
if (! $ao)
throw new ModelNotFoundException('Unknown node: '.$this->argument('ftn'));
Job::dispatchSync($ao,$mo);
Log::info(sprintf('CES:- Call EMSI send for %s',$ao->ftn));
$mo = Mailer::where('name',self::ID)->singleOrFail();
Job::dispatch($ao,$mo);
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Job;
class JobList extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'job:list';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Detail list of items in the queue';
/**
* Execute the console command.
*/
public function handle()
{
$lastq = NULL;
foreach (Job::orderBy('queue')->orderBy('created_at')->cursor() as $o) {
if ($o->queue !== $lastq) {
$this->alert(sprintf('Queue: %s',$o->queue));
$lastq = $o->queue;
}
$this->info(sprintf('%s-%d: %s[%s] - %d tries (Created: %s,Timeout: %s,Next: %s)',
$o->uuid,
$o->id,
$o->display_name,
$o->command->subject,
$o->attempts,
$o->created_at,
$o->retry_until ?: '-',
$o->reserved_at ?: '-',
));
}
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Jobs\MailSend as Job;
class MailSend extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'mail:send'
.' {--T|type=normal : Send crash, normal or both mail}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Trigger a poll to each node with mail queued';
/**
* Execute the console command.
*/
public function handle()
{
switch ($this->option('type')) {
case 'crash':
Log::info('CML:- Triggering polls to send CRASH mail');
Job::dispatchSync(TRUE);
break;
case 'normal':
Log::info('CML:- Triggering polls to send NORMAL mail');
Job::dispatchSync(FALSE);
break;
case 'all':
Log::info('CML:- Triggering polls to send ALL mail');
Job::dispatchSync(NULL);
break;
default:
$this->error('Specify -T crash, normal or all');
}
}
}

View File

@ -2,6 +2,7 @@
namespace App\Console;
use App\Jobs\MailSend;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -24,8 +25,8 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')
// ->hourly();
$schedule->job(new MailSend(TRUE))->everyFiveMinutes()->withoutOverlapping();
$schedule->job(new MailSend(FALSE))->twiceDaily(1,13);
}
/**

View File

@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Notification;
use Illuminate\Support\ViewErrorBag;
use App\Http\Requests\SystemRegister;
use App\Jobs\AddressPoll;
use App\Models\{Address,Echoarea,Filearea,Setup,System,SystemZone,Zone};
use App\Notifications\Netmails\AddressLink;
use App\Rules\{FidoInteger,TwoByteInteger};
@ -279,6 +280,13 @@ class SystemController extends Controller
foreach (['name','location','sysop','hold','phone','address','port','active','method','notes','zt_id','pkt_type'] as $key)
$o->{$key} = $request->post($key);
switch ($request->post('pollmode')) {
case 1: $o->pollmode = FALSE; break;
case 2: $o->pollmode = TRUE; break;
default: $o->pollmode = NULL;
}
$o->autohold = FALSE;
$o->save();
$mailers = collect($request->post('mailer_details'))
@ -653,8 +661,10 @@ class SystemController extends Controller
break;
}
if ($ca->count() && $la=$ca->pop())
if ($ca->count() && $la=$ca->pop()) {
Notification::route('netmail',$la)->notify(new AddressLink(Auth::user()));
AddressPoll::dispatch($la)->delay(15);
}
return view('user.system.register_send')
->with('la',$la)

View File

@ -27,6 +27,14 @@ class SystemRegister extends FormRequest
return Gate::allows($this->so->users->count() ? 'update' : 'register',$this->so);
}
public function messages(): array
{
return [
'hold' => 'Must be Yes or No',
'pollmode' => 'Must be Hold, Normal or Crash',
];
}
/**
* Get the validation rules that apply to the request.
*
@ -64,6 +72,7 @@ class SystemRegister extends FormRequest
$this->so->exists ? [
'active' => 'required|boolean',
'hold' => 'required|boolean',
'pollmode' => 'required|integer|min:0|max:2',
] : [],
));
}

View File

@ -3,24 +3,37 @@
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\ManuallyFailedException;
use Illuminate\Queue\MaxAttemptsExceededException;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use App\Classes\Protocol;
use App\Classes\Protocol\{Binkp,EMSI};
use App\Classes\Sock\SocketClient;
use App\Classes\Sock\SocketException;
use App\Models\{Address,Mailer,Setup};
use App\Notifications\Netmails\PollingFailed;
class AddressPoll implements ShouldQueue
class AddressPoll implements ShouldQueue, ShouldBeUnique
{
private const LOGKEY = 'JAP';
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 5;
public $maxExceptions = 1;
public $failOnTimeout = TRUE;
public const QUEUE = 'poll';
private Address $ao;
private ?Mailer $mo;
@ -28,6 +41,50 @@ class AddressPoll implements ShouldQueue
{
$this->ao = $ao;
$this->mo = $mo;
$this->onQueue(self::QUEUE);
}
public function __get($key): mixed
{
switch ($key) {
case 'address':
return $this->ao;
case 'subject':
return $this->ao->ftn;
default:
return NULL;
}
}
/**
* Because pluck doesnt return __get() defined vars
*
* @param $key
* @return bool
*/
public function __isset($key): bool
{
$keys = ['address'];
return in_array($key,$keys);
}
/**
* Time to wait between tries
*
* @return int[] in seconds
*/
public function backoff(): array
{
return [
60*5, // 5 mins
60*60, // 1 hr
60*60*6, // 6 hrs
60*60*12 // 12 hrs
];
}
/**
@ -35,24 +92,19 @@ class AddressPoll implements ShouldQueue
*/
public function handle()
{
if (! $this->ao->system->mailer_preferred->count() || ($this->mo && (! $this->ao->system->mailer_preferred->find($this->mo))))
throw new \Exception(sprintf('Unable to poll [%s] missing mailer details',$this->ao->ftn));
if (! $this->ao->system->mailer_preferred->count() || ($this->mo && (! $this->ao->system->mailer_preferred->find($this->mo)))) {
$this->fail('Missing mailer details');
return;
}
Log::info(sprintf('%s:- Polling [%s] - attempt [%d]',self::LOGKEY,$this->ao->ftn,$this->attempts()));
foreach ($this->ao->system->mailer_preferred as $o) {
// If we chose a protocol, skip to find the mailer details for it
if ($this->mo && ($o->id !== $this->mo->id))
continue;
Log::info(sprintf('%s:- Starting a [%s] session to [%s:%d]',self::LOGKEY,$o->name,$this->ao->system->address,$o->pivot->port));
try {
$client = SocketClient::create($this->ao->system->address,$o->pivot->port);
} catch (SocketException $e) {
Log::error(sprintf('%s:! Unable to connect to [%s]: %s',self::LOGKEY,$this->ao->ftn,$e->getMessage()));
exit(1);
}
switch ($o->name) {
case 'BINKP':
$s = new Binkp(Setup::findOrFail(config('app.id')));
@ -67,16 +119,59 @@ class AddressPoll implements ShouldQueue
break;
default:
throw new \Exception(sprintf('Node [%s] has a mailer type that is unhandled',$this->ao->ftn));
$this->fail('Mailer type unhandled');
return;
}
if (($s->session($session,$client,$this->ao) & Protocol::S_MASK) === Protocol::S_OK) {
Log::info(sprintf('%s:= Connection ended successfully with [%s]',self::LOGKEY,$client->address_remote));
break;
Log::info(sprintf('%s:- Trying a [%s] session to [%s:%d] (%s)',
self::LOGKEY,$o->name,$this->ao->system->address,$o->pivot->port,$this->ao->ftn));
} else {
Log::alert(sprintf('%s:! Connection failed to [%s]',self::LOGKEY,$client->address_remote));
try {
$client = SocketClient::create($this->ao->system->address,$o->pivot->port);
if (($s->session($session,$client,$this->ao) & Protocol::S_MASK) === Protocol::S_OK) {
Log::info(sprintf('%s:= Connection ended successfully with [%s] (%s)',self::LOGKEY,$client->address_remote,$this->ao->ftn));
return;
} else {
Log::alert(sprintf('%s:! Connection failed to [%s] (%s)',self::LOGKEY,$client->address_remote,$this->ao->ftn));
}
} catch (SocketException $e) {
Log::error(sprintf('%s:! Unable to connect to [%s]: %s',self::LOGKEY,$this->ao->ftn,$e->getMessage()));
break;
}
}
$delay = (int)($this->backoff()[$this->attempts()-1] ?? last($this->backoff()));
Log::info(sprintf('%s:= Retrying poll in %d seconds',self::LOGKEY,$delay));
$this->release($delay);
}
public function failed(\Throwable $exception): void
{
switch (get_class($exception)) {
case ManuallyFailedException::class:
Log::error(sprintf('%s:! Address Poll failed for [%s] (%s)',self::LOGKEY,$this->ao->ftn,$exception->getMessage()));
break;
case MaxAttemptsExceededException::class:
Log::error(sprintf('%s:! Address Poll was tried too many times for [%s]',self::LOGKEY,$this->ao->ftn));
Notification::route('netmail',$this->ao)->notify(new PollingFailed);
$this->ao->system->autohold = TRUE;
$this->ao->system->save();
exit(0);
default:
Log::error(sprintf('%s:! Address Poll to [%s] with an unknown exception [%s]',self::LOGKEY,$this->ao->ftn,$exception->getMessage()));
}
}
public function uniqueId(): string
{
return $this->ao->id;
}
}

179
app/Jobs/MailSend.php Normal file
View File

@ -0,0 +1,179 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Repat\LaravelJobs\Job;
use App\Classes\FTN\Message;
use App\Models\Address;
class MailSend implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private const LOGKEY = 'JCM';
private ?bool $crash;
/**
* @param bool $crash Send crash mail only
*/
public function __construct(bool $crash=NULL)
{
$this->crash = $crash;
}
/**
* Execute the job.
*/
public function handle(): void
{
// Netmail waiting by node (only netmail is routed)
$netmails = Address::select(['addresses.id','addresses.zone_id','region_id','host_id','node_id','point_id','role','addresses.system_id',DB::raw('count(netmails.id) AS nm')])
->join('zones',['zones.id'=>'addresses.zone_id'])
->join('domains',['domains.id'=>'zones.domain_id'])
->join('netmails',['netmails.tftn_id'=>'addresses.id'])
->join('systems',['systems.id'=>'addresses.system_id'])
->where('addresses.active',TRUE)
->where('zones.active',TRUE)
->where('domains.active',TRUE)
->where(function($query) {
return $query->whereNull('autohold')
->orWhere('autohold',FALSE);
})
->where(function($query) {
return $query->whereRaw(sprintf('(flags & %d) > 0',Message::FLAG_INTRANSIT))
->orWhereRaw(sprintf('(flags & %d) > 0',Message::FLAG_LOCAL));
})
->whereRaw(sprintf('(flags & %d) = 0',Message::FLAG_SENT))
->when(! is_null($this->crash),function($query) {
return $query->when(
$this->crash,
function($query) {
return $query->where('pollmode',$this->crash);
},
function($query) {
return $query->whereNotNull('pollmode');
}
);
})
->groupBy('addresses.id')
->havingRaw('count(*) > 0')
->get();
// Echomail waiting by node
$echomails = Address::select(['addresses.id','addresses.zone_id','region_id','host_id','node_id','point_id','role','addresses.system_id',DB::raw('count(*) AS em')])
->distinct()
->join('zones',['zones.id'=>'addresses.zone_id'])
->join('domains',['domains.id'=>'zones.domain_id'])
->join('echomail_seenby',['echomail_seenby.address_id'=>'addresses.id'])
->join('systems',['systems.id'=>'addresses.system_id'])
->where('addresses.active',TRUE)
->where('zones.active',TRUE)
->where('domains.active',TRUE)
->where(function($query) {
return $query->whereNull('autohold')
->orWhere('autohold',FALSE);
})
->whereNotNull('export_at')
->whereNull('sent_at')
->when(! is_null($this->crash),function($query) {
return $query->when(
$this->crash,
function($query) {
return $query->where('pollmode',$this->crash);
},
function($query) {
return $query->whereNotNull('pollmode');
}
);
})
->groupBy(['addresses.id'])
->havingRaw('count(*) > 0')
->FTNOrder()
->get();
// Files waiting by node
$files = Address::select(['addresses.id','addresses.zone_id','region_id','host_id','node_id','point_id','role','addresses.system_id',DB::raw('count(*) AS fs')])
->distinct()
->join('zones',['zones.id'=>'addresses.zone_id'])
->join('domains',['domains.id'=>'zones.domain_id'])
->join('file_seenby',['file_seenby.address_id'=>'addresses.id'])
->join('systems',['systems.id'=>'addresses.system_id'])
->where('addresses.active',TRUE)
->where('zones.active',TRUE)
->where('domains.active',TRUE)
->where(function($query) {
return $query->whereNull('autohold')
->orWhere('autohold',FALSE);
})
->whereNotNull('export_at')
->whereNull('sent_at')
->when(! is_null($this->crash),function($query) {
return $query->when(
$this->crash,
function($query) {
return $query->where('pollmode',$this->crash);
},
function($query) {
return $query->whereNotNull('pollmode');
}
);
})
->groupBy(['addresses.id'])
->havingRaw('count(*) > 0')
->FTNOrder()
->get();
// Merge our netmails
foreach ($echomails as $ao) {
if (($x=$netmails->search(function($item) use ($ao) { return $item->id === $ao->id; })) !== FALSE) {
$netmails->get($x)->em = $ao->em;
} else {
$netmails->push($ao);
}
}
// Merge our files
foreach ($files as $ao) {
if (($x=$netmails->search(function($item) use ($ao) { return $item->id === $ao->id; })) !== FALSE) {
$netmails->get($x)->fs = $ao->fs;
} else {
$netmails->push($ao);
}
}
// Remove direct links
$netmails = $netmails->filter(function($item) { return $item->parent(); });
foreach ($netmails->groupBy(function($item) { return $item->parent()->ftn; }) as $oo) {
$ao = $oo->first();
Log::info(sprintf('%s:- Polling [%s] - we have mail for [%d] links. (%d Netmail,%d Echomail,%d Files)',
self::LOGKEY,
$ao->ftn,
$oo->count(),
$oo->sum('nm'),
$oo->sum('em'),
$oo->sum('fs'),
));
// @todo Only send crash mail - send normal mail with a schedule or hold mail
if (Job::where('queue',$this->queue)->get()->pluck('command.address.id')->search($ao->id) !== FALSE) {
Log::alert(sprintf('%s:= Not scheduling poll to [%s], there is already one in the queue',self::LOGKEY,$ao->ftn));
continue;
}
AddressPoll::dispatch($ao);
}
}
}

View File

@ -2,7 +2,6 @@
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -283,7 +282,7 @@ class Address extends Model
return NULL;
default:
throw new \Exception('Unknown role: '.serialize($this->role));
throw new \Exception(sprintf('Unknown role: %s (%d)',serialize($this->role),$this->id));
}
return $parent?->parent();

69
app/Models/Job.php Normal file
View File

@ -0,0 +1,69 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Config;
class Job extends Model
{
public $timestamps = false;
protected $casts = [
'payload' => 'array',
'reserved_at' => 'datetime',
'available_at' => 'datetime',
'created_at' => 'datetime',
];
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->table = Config::get('queue.connections.' . (Config::get('queue.default', 'database')) . '.table', 'jobs');
}
public function getDisplayNameAttribute()
{
return $this->payload['displayName'];
}
public function getMaxTriesAttribute()
{
return $this->payload['maxTries'];
}
public function getDelayAttribute()
{
return $this->payload['delay'];
}
public function getUUIDAttribute()
{
return $this->payload['uuid'];
}
public function getTimeoutAttribute()
{
return $this->payload['timeout'];
}
public function getRetryUntilAttribute()
{
return !is_null($this->payload['retryUntil']) ? new \Carbon\Carbon($this->payload['retryUntil']) : null;
}
public function getTimeoutAtAttribute()
{
return !is_null($this->payload['timeout_at']) ? new \Carbon\Carbon($this->payload['timeout_at']) : null;
}
public function getCommandNameAttribute()
{
return $this->payload['data']['commandName'];
}
public function getCommandAttribute()
{
return unserialize($this->payload['data']['command']);
}
}

View File

@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use App\Jobs\AddressPoll;
class System extends Model
{
use HasFactory;
@ -214,4 +216,13 @@ class System extends Model
return ($item->role & $type) && ($myzones->search($item->zone_id) !== FALSE);
});
}
public function poll(): ?Job
{
return Job::where('queue',AddressPoll::QUEUE)
->get()
->where(function($item) {
return $this->akas->pluck('id')->search($item->command->address->id) !== FALSE; })
->last();
}
}

View File

@ -5,7 +5,6 @@ namespace App\Notifications\Channels;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Log;
use App\Jobs\AddressPoll as Job;
use App\Models\Netmail;
use App\Models\Setup;
@ -43,7 +42,6 @@ class NetmailChannel
$so = Setup::findOrFail(config('app.id'))->system;
$o = $notification->toNetmail($so,$notifiable);
Job::dispatch($ao);
Log::info(sprintf('%s:= Sent netmail [%s] to [%s]',self::LOGKEY,$o->msgid,$ao->ftn));
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Notifications\Netmails;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use App\Classes\FTN\Message;
use App\Notifications\Netmails;
use App\Models\{Netmail,System};
use App\Traits\{MessagePath,PageTemplate};
class PollingFailed extends Netmails
{
use MessagePath,PageTemplate;
private const LOGKEY = 'NPF';
private Carbon $attempt;
/**
* Get the mail representation of the notification.
*
* @param System $so
* @param mixed $notifiable
* @return Netmail
* @throws \Exception
*/
public function toNetmail(System $so,object $notifiable): Netmail
{
$o = $this->setupNetmail($so,$notifiable);
$ao = $notifiable->routeNotificationFor(static::via);
Log::info(sprintf('%s:+ Creating auto hold netmail to [%s]',self::LOGKEY,$ao->ftn));
$o->subject = 'Failed polling - your system is on AUTO HOLD';
// Message
$msg = $this->page(FALSE,'hold');
$msg->addText("Hi, I've been trying to poll your system without success.\r\r");
$msg->addText("Your system was automatically placed on hold, which means I no longer attempted to poll you.\r\r");
$msg->addText(
'Since you collected this message, I automatically removed the auto hold status, but if future attempts to poll you fail '.
"you'll be automatically placed back on auto hold until you poll me. You'll also get this annoying message each time :(\r\r");
$msg->addText("To fix this, update your details that I use in the web interface, or change your system to HOLD while you are there.\r\r");
$o->msg = $msg->render();
$o->tagline = 'Painful? We can make that painless :)';
$o->save();
return $o;
}
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('system_logs',function (Blueprint $table) {
$table->boolean('originate')->nullable();
});
Schema::table('systems',function (Blueprint $table) {
$table->boolean('pollmode')->nullable();
$table->boolean('autohold')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('systems',function (Blueprint $table) {
$table->dropColumn(['pollmode','autohold']);
});
Schema::table('system_logs',function (Blueprint $table) {
$table->dropColumn(['originate']);
});
}
};

View File

@ -272,8 +272,8 @@
<table class="table monotable">
<thead>
<tr>
<th>Zone</th>
<th>System</th>
<th>AKA</th>
<th>Uplink</th>
</tr>
</thead>
@ -286,7 +286,7 @@
@if ($x=$oo->parent())
{{ $x->ftn4d }}
@else
No destination for mail.
None
@endif
</td>
</tr>
@ -305,8 +305,8 @@
<table class="table monotable">
<thead>
<tr>
<th>Zone</th>
<th>System</th>
<th>AKA</th>
<th>Downlink</th>
</tr>
</thead>

View File

@ -1,3 +1,4 @@
<!-- $o = System::class -->
@php
use App\Models\Setup;
@endphp
@ -21,8 +22,22 @@
</div>
</div>
<!-- ZeroTier ID -->
<div class="col-3">
<label for="zt_id" class="form-label">ZeroTier ID</label>
<div class="input-group has-validation">
<span class="input-group-text"><i class="bi bi-shield-lock-fill"></i></span>
<input type="text" class="form-control @error('zt_id') is-invalid @enderror" id="zt_id" placeholder="ZeroTier" name="zt_id" value="{{ old('zt_id',$o->zt_id) }}" @cannot($action,$o)readonly @endcannot>
<span class="invalid-feedback" role="alert">
@error('zt_id')
{{ $message }}
@enderror
</span>
</div>
</div>
<!-- Active -->
<div class="col-2">
<div class="offset-2 col-2">
@can('update',$o)
<label for="active" class="form-label">Active</label>
<div class="input-group">
@ -36,36 +51,6 @@
</div>
@endcan
</div>
<!-- Hold -->
<div class="col-2">
@can('update',$o)
<label for="hold" class="form-label">Hold Mail</label>
<div class="input-group">
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="hold" id="hold_yes" value="1" required @if(old('hold',$o->hold))checked @endif>
<label class="btn btn-outline-success" for="hold_yes">Yes</label>
<input type="radio" class="btn-check btn-danger" name="hold" id="hold_no" value="0" required @if(! old('hold',$o->hold))checked @endif>
<label class="btn btn-outline-danger" for="hold_no">No</label>
</div>
</div>
@endcan
</div>
<!-- ZeroTier ID -->
<div class="offset-1 col-3">
<label for="zt_id" class="form-label">ZeroTier ID</label>
<div class="input-group has-validation">
<span class="input-group-text"><i class="bi bi-shield-lock-fill"></i></span>
<input type="text" class="form-control @error('zt_id') is-invalid @enderror" id="zt_id" placeholder="ZeroTier" name="zt_id" value="{{ old('zt_id',$o->zt_id) }}" @cannot($action,$o)readonly @endcannot>
<span class="invalid-feedback" role="alert">
@error('zt_id')
{{ $message }}
@enderror
</span>
</div>
</div>
</div>
<div class="row">
@ -86,7 +71,7 @@
</div>
<!-- Location -->
<div class="col-8">
<div class="col-4">
<label for="location" class="form-label">Location</label>
<div class="input-group has-validation">
<span class="input-group-text"><i class="bi bi-globe"></i></span>
@ -100,10 +85,41 @@
</span>
</div>
</div>
<!-- Hold -->
<div class="offset-1 col-2">
@can('update',$o)
<label for="hold" class="form-label">Hold Mail <i class="bi bi-info-circle" title="Dont give the node any mail regardless of poll mode"></i></label>
<div class="input-group">
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="hold" id="hold_yes" value="1" required @if(old('hold',$o->hold))checked @endif>
<label class="btn btn-outline-warning" for="hold_yes">Yes</label>
<input type="radio" class="btn-check btn-danger" name="hold" id="hold_no" value="0" required @if(! old('hold',$o->hold))checked @endif>
<label class="btn btn-outline-success" for="hold_no">No</label>
</div>
</div>
@endcan
</div>
</div>
<div class="row">
<div class="col-4">
<!-- Address -->
<div class="col-5">
<label for="address" class="form-label">Internet Address</label>
<div class="input-group has-validation">
<span class="input-group-text"><i class="bi bi-globe"></i></span>
<input type="text" class="w-75 form-control @error('address') is-invalid @enderror" id="address" placeholder="FQDN" name="address" value="{{ old('address',$o->address) }}" @cannot($action,$o)readonly @endcannot>
<span class="invalid-feedback" role="alert">
@error('address')
{{ $message }}
@enderror
</span>
</div>
</div>
<!-- Phone -->
<div class="col-3">
<label for="phone" class="form-label">Phone</label>
<div class="input-group has-validation">
<span class="input-group-text"><i class="bi bi-telephone-fill"></i></span>
@ -116,18 +132,28 @@
</div>
</div>
<!-- Address -->
<div class="col-8">
<label for="address" class="form-label">Internet Address</label>
<div class="input-group has-validation">
<span class="input-group-text"><i class="bi bi-globe"></i></span>
<input type="text" class="w-75 form-control @error('address') is-invalid @enderror" id="address" placeholder="FQDN" name="address" value="{{ old('address',$o->address) }}" @cannot($action,$o)readonly @endcannot>
<span class="invalid-feedback" role="alert">
@error('address')
{{ $message }}
@enderror
</span>
</div>
<!-- Poll Mode -->
<div class="offset-1 col-3">
@can('update',$o)
<label for="pollmode" class="form-label">Poll Mode <i class="bi bi-info-circle" title="Poll node when mail available, poll on a schedule or hold mail for collection"></i></label>
<div class="input-group has-validation">
<div class="btn-group @error('pollmode') is-invalid @enderror" role="group">
<input type="radio" class="btn-check" name="pollmode" id="poll_crash" value="2" @if((int)old('pollmode',($o->pollmode === TRUE) ? 2 : 0) === 2)checked @endif>
<label class="btn btn-outline-success" for="poll_crash">Crash</label>
<input type="radio" class="btn-check btn-danger" name="pollmode" id="poll_normal" value="1" @if((int)old('pollmode',($o->pollmode === FALSE) ? 1 : 0) === 1)checked @endif>
<label class="btn btn-outline-secondary" for="poll_normal">Normal</label>
<input type="radio" class="btn-check btn-danger" name="pollmode" id="poll_hold" value="0" @if((int)old('pollmode',is_null($o->pollmode) ? 0 : 1) === 0)checked @endif>
<label class="btn btn-outline-warning" for="poll_hold">Hold</label>
</div>
<span class="invalid-feedback" role="alert">
@error('pollmode')
{{ $message }}
@enderror
</span>
</div>
@endcan
</div>
</div>
@ -136,6 +162,7 @@
<div class="col-12">
<h4 class="pt-4 mb-0 pb-2">Mailer Details</h4>
<!-- Mailer Ports -->
<div class="pt-0 row">
<div class="col-3">
@foreach (\App\Models\Mailer::all() as $mo)
@ -160,6 +187,7 @@
@endforeach
</div>
<!-- Mail Packet -->
<div class="col-2">
<label for="pkt_type" class="form-label">Mail Packet</label>
<div class="input-group">
@ -177,6 +205,55 @@
</div>
</div>
@if (! is_null($o->pollmode))
<div class="offset-3 col-4">
<table class="table monotable m-0 p-0 small noborder">
<tbody style="border-style:dotted;">
@if($job = $o->poll())
<tr>
<td class="cap text-end">@if($job->attempts)Last Attempt @else Scheduled @endif:</td>
<td>{{ $job->created_at }} </td>
</tr>
<tr>
<td class="cap text-end">Attempts :</td>
<td>{{ $job->attempts ?: 0 }}</td>
</tr>
@if ($job->attempts)
<tr>
<td class="cap text-end">Next Attempt :</td>
<td>{{ $job->available_at->diffForHumans(now(),$job->available_at->isFuture() ? \Carbon\CarbonInterface::DIFF_ABSOLUTE : \Carbon\CarbonInterface::DIFF_RELATIVE_TO_NOW) }}</td>
</tr>
@endif
@else
<tr>
<td class="cap text-end">Last Poll :</td>
<td>{{ ($x=$o->logs->where('originate',TRUE)->last())?->created_at ?: 'Never' }}</td>
</tr>
<tr>
<td class="cap text-end">Method :</td>
<td>{{ $x?->sessiontype ?: '-' }}</td>
</tr>
@endif
<tr>
<td class="cap text-end">Status :</td>
<td>
@if ($job) Queued
@elseif ($o->autohold)Auto Hold
@else
@switch($o->pollmode)
@case(TRUE) Crash @break;
@case(FALSE) Normal @break;
@default Hold
@endswitch
@endif
</td>
</tr>
</tbody>
</table>
</div>
@endif
</div>
</div>
</div>