28 Commits

Author SHA1 Message Date
e8b9517664 Added Hellcase Battle job
All checks were successful
Push image to registry / build-image (push) Successful in 5m59s
Still need testing and making proper notifications
2025-03-18 19:40:55 +01:00
cfbae6ddbf Only render jobs run if there is at least one
All checks were successful
Push image to registry / build-image (push) Successful in 4m10s
2025-03-16 17:37:39 +01:00
4368aae6c4 install missing dependencies
All checks were successful
Push image to registry / build-image (push) Successful in 5m25s
2025-03-15 17:49:53 +01:00
070235e011 Added jobRuns on the job page
Some checks failed
Push image to registry / build-image (push) Failing after 3m47s
2025-03-15 17:44:30 +01:00
ad10dcaa0f Fix joining giveaways 2025-03-15 16:26:29 +01:00
63216ae7e8 Fix pls
All checks were successful
Push image to registry / build-image (push) Successful in 8m20s
2025-03-11 21:04:53 +01:00
4a68c1d223 Fix signin ?
All checks were successful
Push image to registry / build-image (push) Successful in 6m33s
2025-03-11 20:38:15 +01:00
d48c0cd148 Try to fix login
All checks were successful
Push image to registry / build-image (push) Successful in 6m32s
2025-03-11 20:25:00 +01:00
8e356ac1ef Click the next button
All checks were successful
Push image to registry / build-image (push) Successful in 8m26s
2025-03-11 19:33:44 +01:00
a693339f97 Get daily free even if giveaway fails
All checks were successful
Push image to registry / build-image (push) Successful in 6m51s
2025-03-11 18:46:45 +01:00
5bfaba4a8e try to fix unclickable giveway button
Some checks failed
Push image to registry / build-image (push) Has been cancelled
2025-03-11 18:45:54 +01:00
140966fb6b fix should start maximized ?
All checks were successful
Push image to registry / build-image (push) Successful in 5m28s
2025-03-08 15:43:22 +01:00
e3443ca632 update composer lock
All checks were successful
Push image to registry / build-image (push) Successful in 5m13s
2025-03-02 13:51:44 +01:00
17af60471b Fix Notifications
Some checks failed
Push image to registry / build-image (push) Failing after 4m19s
2025-03-02 13:31:26 +01:00
0014045d52 Fix job 2025-03-02 13:29:44 +01:00
fec63ff249 Added Debug notifications
Some checks failed
Push image to registry / build-image (push) Failing after 3m41s
2025-03-02 10:03:27 +01:00
cdcdcd9e8b Wait for daily free if less than a minute
Some checks failed
Push image to registry / build-image (push) Failing after 3m45s
2025-03-02 09:40:07 +01:00
599ef77d64 Should fix steam connexion
Some checks failed
Push image to registry / build-image (push) Failing after 6m50s
2025-03-02 09:08:44 +01:00
edb07dd960 Upgrade to Laravel 12
Some checks failed
Push image to registry / build-image (push) Failing after 3m42s
2025-03-01 15:31:11 +01:00
6a95653c52 Added notification on job fail
All checks were successful
Push image to registry / build-image (push) Successful in 5m20s
2025-03-01 15:12:15 +01:00
025711e09d Updated dockerignore
All checks were successful
Push image to registry / build-image (push) Successful in 5m1s
2025-03-01 12:49:02 +01:00
5a30cdeae5 Made a script to patch latest selenium/standalone-chromedriver 2025-03-01 12:33:03 +01:00
db9d65f445 Fix job ordering 2025-03-01 10:02:47 +01:00
a1219b92ce Should fix
All checks were successful
Push image to registry / build-image (push) Successful in 7m31s
2025-02-28 17:05:47 +01:00
ce13d1b0dd Job qui enlève les vieilles jobRun
All checks were successful
Push image to registry / build-image (push) Successful in 6m18s
2025-02-27 18:54:40 +01:00
a80a32eee8 Added links to browser downloads and others
All checks were successful
Push image to registry / build-image (push) Successful in 6m20s
2025-02-27 17:57:54 +01:00
cd3491c5a8 Install reverb for broadcasting
All checks were successful
Push image to registry / build-image (push) Successful in 3m56s
2025-02-26 18:52:12 +01:00
9e55c98d47 Added TODO list 2025-02-26 18:28:42 +01:00
63 changed files with 2698 additions and 245 deletions

View File

@ -14,6 +14,7 @@ database/database.sqlite
**/.dockerignore
**/.env
**/.git
.gitea/
**/.gitignore
**/.project
**/.settings
@ -67,3 +68,5 @@ yarn-error.log
app/Browser/console/**
app/Browser/screenshots/**
app/Browser/source/**
undetectedChromedriver

View File

@ -69,3 +69,15 @@ VITE_APP_URL="${APP_URL}"
DUSK_DRIVER_URL="http://undetected-chromedriver:4444"
DUSK_START_MAXIMIZED=true
# Reverb (broadcasting)
REVERB_APP_ID=943562
REVERB_APP_KEY=jzysqfijn8wdgugrilbb
REVERB_APP_SECRET=gyotb3asui2zbrltd3ob
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"

View File

@ -68,3 +68,16 @@ VITE_APP_URL="${APP_URL}"
DUSK_DRIVER_URL="http://127.0.0.1:4444"
DUSK_START_MAXIMIZED=true
# Reverb (broadcasting)
REVERB_APP_ID=943562
REVERB_APP_KEY=jzysqfijn8wdgugrilbb
REVERB_APP_SECRET=gyotb3asui2zbrltd3ob
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"

5
.gitignore vendored
View File

@ -21,6 +21,11 @@ yarn-error.log
/.nova
/.vscode
/.zed
# Python projet
venv
__pycache__
# Browser
app/Browser/console
app/Browser/screenshots

View File

@ -5,6 +5,8 @@ namespace App\Browser;
use App\Models\Job;
use App\Models\JobArtifact;
use App\Models\JobRun;
use App\Notification\Notifications\JobErrorNotification;
use App\Notification\Providers\AllNotification;
use Closure;
use Exception;
use Facebook\WebDriver\Chrome\ChromeOptions;
@ -56,7 +58,8 @@ abstract class BrowserJob implements ShouldQueue
// throw $e;
}
catch (Throwable $e) {
$browser->screenshot("failure-{$this->jobId}");
$browser->screenshot(JobErrorScreenshot::getFileName($this->jobId));
AllNotification::send(new JobErrorNotification($this->jobId, $e->getMessage()));
dump($e);
throw $e;
} finally {
@ -178,8 +181,7 @@ abstract class BrowserJob implements ShouldQueue
*/
protected function hasHeadlessDisabled(): bool
{
return isset($_SERVER['DUSK_HEADLESS_DISABLED']) ||
isset($_ENV['DUSK_HEADLESS_DISABLED']);
return config('dusk.headlessDisabled', false);
}
/**
@ -187,8 +189,7 @@ abstract class BrowserJob implements ShouldQueue
*/
protected function shouldStartMaximized(): bool
{
return isset($_SERVER['DUSK_START_MAXIMIZED']) ||
isset($_ENV['DUSK_START_MAXIMIZED']);
return config('dusk.shouldStartMaximized', false);
}
/**

View File

@ -0,0 +1,27 @@
<?php
namespace App\Browser;
use Laravel\Dusk\Browser;
use function rtrim;
class JobDebugScreenshot {
public const IMG_FILE_NAME = "debug-";
public static function getFileName(int $jobId): string {
return static::IMG_FILE_NAME . $jobId . ".png";
}
public static function getImgFileAbsolutePath(int $jobId): string {
return rtrim(Browser::$storeScreenshotsAt, '/') . "/" . static::getFileName($jobId);
}
public static function getImgFileProjectPath(int $jobId): string {
return app_path("Browser/screenshots/" . static::getFileName($jobId));
}
public static function getImgFileExternalPath(int $jobId): string {
return "screenshots/" . static::getFileName($jobId);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Browser;
use Laravel\Dusk\Browser;
use function rtrim;
class JobErrorScreenshot {
public const IMG_FILE_NAME = "failure-";
public static function getFileName(int $jobId): string {
return static::IMG_FILE_NAME . $jobId . ".png";
}
public static function getImgFileAbsolutePath(int $jobId): string {
return rtrim(Browser::$storeScreenshotsAt, '/') . "/" . static::getFileName($jobId);
}
public static function getImgFileProjectPath(int $jobId): string {
return app_path("Browser/screenshots/" . static::getFileName($jobId));
}
public static function getImgFileExternalPath(int $jobId): string {
return "screenshots/" . static::getFileName($jobId);
}
}

View File

@ -4,6 +4,7 @@ namespace App\Browser\Jobs\Hellcase;
use App\Browser\BrowserJob;
use App\Browser\Components\Hellcase\MainNav;
use App\Browser\JobDebugScreenshot;
use App\Browser\Jobs\Hellcase\HellcaseLoginQrCode;
use App\Models\JobArtifact;
use App\Models\JobRun;
@ -11,6 +12,8 @@ use App\Notification\NotificationBody\Hellcase\HellcaseNotificationDailyFreeBody
use App\Notification\NotificationBody\Hellcase\HellcaseNotificationLoginBody;
use App\Notification\Notifications\Hellcase\HellcaseNotificationDailyFree;
use App\Notification\Notifications\Hellcase\HellcaseNotificationLogin;
use App\Notification\Notifications\JobDebugNotification;
use App\Notification\Notifications\JobErrorNotification;
use App\Notification\Providers\AllNotification;
use Facebook\WebDriver\WebDriverBy;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
@ -22,12 +25,12 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
private const STEAM_LOGIN_THRESHOLD = 5 * 60; // 5 minutes
private const APPROXIMATIVE_RUNNING_MINUTES = 2;
private JobRun $jobRun;
protected JobRun $jobRun;
public function __construct()
public function __construct($jobId = 2)
{
Log::info("Constructing HellcaseJob");
parent::__construct(2);
parent::__construct($jobId);
}
public function run(Browser $browser): ?JobRun
@ -40,11 +43,17 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
$this->jobRun->save();
$browser->visit('https://hellcase.com');
$browser->waitForText("Store", 30, true);
$browser->waitForText("CASES", 30, true);
$this->removePopups($browser);
sleep(5);
$this->signin($browser);
try {
$this->joinFreeGiveaways($browser);
} catch (\Exception $e) {
$this->jobRun->success = false;
$this->jobRun->save();
AllNotification::send(new JobErrorNotification($this->jobId, "Erreur lors de la participation aux concours gratuits : " . $e->getMessage()));
}
$this->getDailyFree($browser);
$this->jobRun->success = true;
@ -80,7 +89,7 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
}
}
private function signin(Browser $browser)
protected function signin(Browser $browser)
{
try {
$browser->clickAtXPath('//button[.//span[text() = "Sign in"]]');
@ -91,13 +100,20 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
sleep(5);
$browser->waitForText("Sign in with Steam", 30, true);
sleep(3);
$browser->driver->findElement(WebDriverBy::xpath('//button[@class = "_base_1uydq_1 _accent-1_1uydq_105 _m_1uydq_52 _full_1uydq_94 _primary_1uydq_100"]'))->click();
$browser->driver->findElement(WebDriverBy::xpath('//button[contains(@class,"_base_zvftr_1 _accent-1_zvftr_105 _m_zvftr_52 _full_zvftr_94 _primary_zvftr_100")]'))->click();
sleep(5);
// QR CODE SCANNING
try {
$browser->waitForTextIn("div", "Or sign in with QR", 30, true);
sleep(10);
try {
$qrCode = $browser->driver->findElement(WebDriverBy::xpath('//div[./*[contains(text(), "Or sign in with QR")]]'));
} catch (\Exception $e) {
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
AllNotification::send(new JobDebugNotification($this->jobId, "Le QR code de la page de connexion de Steam n'a pas été trouvé"));
throw $e;
}
// Wait to be redirected to the Steam login page, while waiting take a new screenshot every 30 seconds
$isBackOnHellcase = false;
@ -122,9 +138,11 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
}
} catch (\Exception $e) {
// If the QR code is not found, we are not on the QR code page
Log::debug("Exception because qrcode not found : " . $e);
$isBackOnHellcase = true;
} catch (\Throwable $e) {
// If the QR code is not found, we are not on the QR code page
Log::debug("Exception because qrcode not found : " . $e);
$isBackOnHellcase = true;
}
@ -141,6 +159,8 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
try {
$buttons = $browser->driver->findElements(WebDriverBy::xpath('//a[text() = "Join for free"]'));
} catch (\Exception $e) {
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
AllNotification::send(new JobDebugNotification($this->jobId, "No join for free buttons found"));
return;
}
@ -150,9 +170,31 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
"content" => ""
]));
}
try {
$nextSlideButton = $browser->driver->findElement(WebDriverBy::xpath('//button[@class="_button_1ygbm_7 _next_1ygbm_24"]'));
} catch (\Exception $e) {
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
AllNotification::send(new JobDebugNotification($this->jobId, "No next slide button found"));
return;
}
foreach ($buttons as $button) {
// Click the next slide button if the button is not clickable
$clickedFailsCounter = 0;
while ($clickedFailsCounter < 7 && $clickedFailsCounter >= 0) {
try {
$button->click();
sleep(5);
} catch (\Exception $e) {
$clickedFailsCounter++;
try {
$nextSlideButton->click();
} catch (\Exception $_) {}
sleep(3);
continue;
}
$clickedFailsCounter = -1;
}
sleep(5); // Wait a bit for loading
$this->joinGiveaway($browser);
$browser->within(new MainNav, function (Browser $browser) {
$browser->goToHome();
@ -196,9 +238,12 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
if ($availibleInButton->getAttribute("disabled") == "true") {
$hours = $availibleInButton->getText();
// If the text is like "in 26 sec." we need to put one minute
if (str_contains($hours, "sec")) {
$this->reschedule(1);
return;
if (str_contains(strtolower($hours), "seconds")) {
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
AllNotification::send(new JobDebugNotification($this->jobId, "I hate niggers"));
// $this->reschedule(1);
sleep(60);
return $this->getDailyFree($browser);
}
$hours = explode(" ", $hours);
$minutes = $hours[4];
@ -345,7 +390,7 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
$browser->clickAtXPath('//*[contains(text(), "Edit Profile")]');
}
private function removePopups(Browser $browser)
protected function removePopups(Browser $browser)
{
// $browser->script('document.querySelector("div.app-modal")[0].remove();');
// $browser->driver->executeScript('document.querySelector("div.app-modal")[0].remove();');

View File

@ -0,0 +1,145 @@
<?php
namespace App\Browser\Jobs\HellcaseBattles;
use App\Browser\Jobs\Hellcase\HellcaseJob;
use App\Models\HellcaseBattle;
use App\Models\Job;
use App\Models\JobRun;
use App\Notification\Notifications\JobDebugNotification;
use App\Notification\Providers\AllNotification;
use Exception;
use Facebook\WebDriver\WebDriver;
use Facebook\WebDriver\WebDriverBy;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Laravel\Dusk\Browser;
class HellcaseBattlesJob extends HellcaseJob implements ShouldBeUniqueUntilProcessing
{
private Collection $jobInfos;
private array $battlesToAdd = [];
public function __construct()
{
Log::info("Constructing HellcaseBattlesJob");
parent::__construct(3);
}
public function run(Browser $browser): ?JobRun
{
$this->jobInfos = Job::find($this->jobId)->jobInfosTable();
Log::info("Running HellcaseBattlesJob");
$this->jobRun = new JobRun([
"job_id" => $this->jobId,
"success" => false,
]);
$this->jobRun->save();
$browser->visit('https://hellcase.com');
$browser->waitForText("CASES", 30, true);
$this->removePopups($browser);
sleep(5);
$this->signin($browser);
$this->saveInterestingBattles($browser);
$this->sendFinishedBattles($browser);
$this->createNewBattles();
$this->jobRun->success = true;
$this->jobRun->save();
Log::info("HellcaseBattlesJob run ended");
return $this->jobRun;
}
/**
* Save current cases battles to database for later processing
* @param \Laravel\Dusk\Browser $browser
* @return void
*/
private function saveInterestingBattles(Browser $browser)
{
$battleIndex = 0; // Index of the battle to get info from
$running = true;
while ($running) {
$browser->visit('https://hellcase.com/casebattle');
$browser->waitForText("CASES", 30, true);
AllNotification::send(new JobDebugNotification($this->jobId, "I hate niggers"));
// Sort by price
try {
$sortByPriceDiv = $browser->driver->findElement(WebDriverBy::xpath("//*[span[contains(text(), 'Value')]]"));
$sortByPriceDiv->click();
} catch (Exception $e) {
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to sort by price"));
return;
}
sleep(5);
$battles = $browser->driver->findElements(WebDriverBy::xpath("//*[contains(@class, 'casebattle-table__item')]"));
$battle = $battles[$battleIndex];
$battleIndex++;
$browser->scrollIntoView(".casebattle-table__item:nth-child(" . max($battleIndex -1, 1) . ")");
sleep(2);
$battleValue = floatval(
explode(
"\n",
$battle->findElement(WebDriverBy::xpath("./div/div[contains(@class, 'core-price')]"))->getDomProperty("innerText")
)[1]
);
if ($battleValue < floatval($this->jobInfos->get("hellcase_battles_minimum_value"))) {
$running = false;
break;
}
$battleLinkButton = $battle->findElement(WebDriverBy::xpath('./div//button[text() = "watch"]'));
$battleLinkButton->sendKeys("\n");
sleep(3);
$battleLink = $browser->driver->getCurrentURL();
$this->battlesToAdd[$battleLink] = $battleValue;
}
}
private function sendFinishedBattles(Browser $browser) {
// foreach battle that we didn"t already planned to add with $this->battlesToAdd
foreach (HellcaseBattle::all() as $battle) {
dump($battle);
if (!array_key_exists($battle->getUrl(), $this->battlesToAdd)) {
dump("finished");
$browser->visit($battle->getUrl());
try {
$browser->waitForText("Started at");
// Send the battle
$this->sendBattle($browser, $battle);
} catch (Exception $e) { // Battle is not finished or error (like battle cancelled)
}
$battle->delete();
}
}
}
private function sendBattle(Browser $browser, HellcaseBattle $battle) {
AllNotification::send(new JobDebugNotification($this->jobId, "Battle sent" . $battle->getUrl()));
}
private function createNewBattles() {
foreach ($this->battlesToAdd as $battleLink => $battleValue) {
$battleLink = explode("/", $battleLink);
HellcaseBattle::firstOrCreate([
"battle_id" => $battleLink[count($battleLink) - 1],
"value" => $battleValue,
]);
}
}
}

View File

@ -21,7 +21,7 @@ class JobController extends Controller
public function show($jobId, Request $request)
{
return Inertia::render('Job', [
'job' => Job::where('id', $jobId)->with('jobInfos')->first(),
'job' => Job::where('id', $jobId)->with('jobInfos', 'jobRuns')->first(),
'error' => $request->input('error'),
]);
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Jobs;
use App\Models\Job;
use App\Models\JobRun;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class PruneOldJobRuns implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct()
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
// For each job, keep only the last N runs
foreach (Job::all() as $job) {
$job->jobRuns()
->orderByDesc('id')
->skip(config('jobs.pruneOldJobRuns.max_runs_per_job'))
->delete();
}
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class HellcaseBattle extends Model
{
protected $fillable = [
"battle_id",
"value",
];
public function getUrl() {
return "https://hellcase.com/casebattle/{$this->battle_id}";
}
}

View File

@ -21,6 +21,16 @@ class Job extends Model
return $this->hasMany(JobInfo::class)->with("jobInfoType")->orderBy("created_at");
}
/**
* Get an associative collection of the job infos with their values
* @return \Illuminate\Database\Eloquent\Collection<string, string>>
*/
public function jobInfosTable() {
return $this->jobInfos->mapWithKeys(function ($jobInfo) {
return [$jobInfo->key => $jobInfo->value];
});
}
public function jobRuns()
{
return $this->hasMany(JobRun::class)->orderBy("created_at");

View File

@ -4,6 +4,7 @@ namespace App\Notification;
use App\Models\Job;
use App\Notification\Stringifiable\StringifiableSimpleText;
use function PHPUnit\Framework\isNull;
abstract class Notification {
@ -12,12 +13,18 @@ abstract class Notification {
public bool $isError;
public function __construct(int $jobId, NotificationBody $body, bool $isError = false) {
public function __construct(int $jobId, NotificationBody $body = null, bool $isError = false) {
$this->job = Job::find($jobId);
if ($body !== null) {
$this->body = $body;
}
$this->isError = $isError;
}
public function setBody(NotificationBody $body) {
$this->body = $body;
}
public function getTitle(): Stringifiable {
return new StringifiableSimpleText($this->job->name);
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Notification\NotificationBody;
use App\Models\Job;
use App\Notification\NotificationBody;
use App\Notification\Stringifiable;
class JobDebugNotificationBody extends NotificationBody {
private Job $job;
private string $body;
private ?string $error;
private bool $hasScreenshot;
public function __construct(Job $job, string $body, string $error = null, bool $hasScreenshot = false) {
$this->job = $job;
$this->body = $body;
$this->error = $error;
$this->hasScreenshot = $hasScreenshot;
}
private function constructString(bool $inMarkdown = false) {
$mdBody = "";
if ($this->body !== null) {
$mdBody .= $this->body;
}
if ($this->error !== null) {
$errorWrapper = $inMarkdown ? "```" : "";
$mdBody .= " :\n" . $errorWrapper . $this->error . $errorWrapper;
}
if ($inMarkdown && $this->hasScreenshot) {
$mdBody .= "\nScreenshot : ";
}
return $mdBody;
}
/**
* @inheritDoc
*/
public function toMarkdownString(): string {
return $this->constructString(true);
}
/**
* @inheritDoc
*/
public function toString(): string {
return $this->constructString();
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Notification\NotificationBody;
use App\Models\Job;
use App\Notification\NotificationBody;
use App\Notification\Stringifiable;
class JobErrorNotificationBody extends NotificationBody {
private Job $job;
private string $error;
public function __construct(Job $job, $error) {
$this->job = $job;
$this->error = $error;
}
/**
* @inheritDoc
*/
public function toMarkdownString(): string {
return "Le job \"{$this->job->name}\" a échoué avec l'erreur :\n ```" . $this->error . "``` \nScreenshot : ";
}
/**
* @inheritDoc
*/
public function toString(): string {
return "Le job \"{$this->job->name}\" a échoué avec l'erreur :\n " . $this->error;
}
}

View File

@ -3,12 +3,9 @@
namespace App\Notification\Notifications\Hellcase;
use App\Browser\Jobs\Hellcase\HellcaseDailyFreeScreenshot;
use App\Browser\Jobs\Hellcase\HellcaseLoginQrCode;
use App\Notification\Notification;
use App\Notification\Notifications\NotificationLogin;
use Laravel\Dusk\Browser;
class HellcaseNotificationDailyFree extends NotificationLogin {
class HellcaseNotificationDailyFree extends Notification {
public function __construct(int $jobId, \App\Notification\NotificationBody $body) {
parent::__construct($jobId, $body);

View File

@ -0,0 +1,44 @@
<?php
namespace App\Notification\Notifications;
use App\Browser\JobDebugScreenshot;
use App\Notification\Notification;
use App\Notification\NotificationBody\JobDebugNotificationBody;
use App\Notification\Stringifiable;
use App\Notification\Stringifiable\StringifiableSimpleText;
use Illuminate\Support\Facades\Log;
class JobDebugNotification extends Notification {
private string|null $title;
private string|null $screenShotProjectPath;
public function __construct(int $jobId, string $body, string $title = null, string $error = null, ?string $screenshotProjectPath = "", bool $isError = false) {
parent::__construct($jobId, isError:$isError);
$this->title = $title;
if ($screenshotProjectPath === "") {
$screenshotProjectPath = JobDebugScreenshot::getImgFileProjectPath($jobId);
}
$this->screenShotProjectPath = $screenshotProjectPath;
$this->setBody(new JobDebugNotificationBody($this->job, $body, $error, $this->screenShotProjectPath != null));
}
public function getTitle(): Stringifiable {
return new StringifiableSimpleText($this->title ?? "DEBUG Job {$this->job->name}");
}
/**
* @inheritDoc
*/
public function getImageProjectPath(): string|null {
return $this->screenShotProjectPath;
}
/**
* @inheritDoc
*/
public function getLinkURL(): string|null {
return route('jobs.show', ['job' => $this->job->id]);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Notification\Notifications;
use App\Browser\JobErrorScreenshot;
use App\Models\Job;
use App\Notification\Notification;
use App\Notification\NotificationBody\JobErrorNotificationBody;
use App\Notification\Stringifiable;
use App\Notification\Stringifiable\StringifiableSimpleText;
use Illuminate\Support\Facades\Log;
class JobErrorNotification extends Notification {
public function __construct(int $jobId, string $error) {
parent::__construct($jobId, isError:true);
$this->setBody(new JobErrorNotificationBody($this->job, $error));
}
public function getTitle(): Stringifiable {
return new StringifiableSimpleText("Le job {$this->job->name} a échoué");
}
/**
* @inheritDoc
*/
public function getImageProjectPath(): string|null {
return JobErrorScreenshot::getImgFileProjectPath($this->job->id);
}
/**
* @inheritDoc
*/
public function getLinkURL(): string|null {
return route('jobs.show', ['job' => $this->job->id]);
}
}

View File

@ -3,9 +3,6 @@
namespace App\Notification\Providers;
use App\Notification\NotificationProvider;
use App\Notification\INotificationProvider;
use App\Models\JobInfo;
use Illuminate\Support\Facades\Cache;
class AllNotification extends NotificationProvider {
private const NOTIFICATIONS_PROVIDERS = [

View File

@ -3,7 +3,6 @@
namespace App\Notification\Providers;
use App\Notification\NotificationProvider;
use App\Notification\INotificationProvider;
use App\Models\JobInfo;
use Illuminate\Support\Facades\Cache;

View File

@ -9,6 +9,7 @@ return Application::configure(basePath: dirname(__DIR__))
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
channels: __DIR__.'/../routes/channels.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {

View File

@ -2,7 +2,6 @@
"$schema": "https://shadcn-vue.com/schema.json",
"style": "default",
"typescript": true,
"tsConfigPath": "./tsconfig.json",
"tailwind": {
"config": "tailwind.config.js",
"css": "resources/css/app.css",
@ -10,9 +9,12 @@
"cssVariables": true,
"prefix": ""
},
"framework": "laravel",
"aliases": {
"components": "@/Components",
"utils": "@/lib/utils"
}
"composables": "@/composables",
"utils": "@/lib/utils",
"ui": "@/Components/ui",
"lib": "@/lib"
},
"iconLibrary": "lucide"
}

View File

@ -13,7 +13,8 @@
"erusev/parsedown": "^1.7",
"inertiajs/inertia-laravel": "^2.0",
"laravel/dusk": "^8.2",
"laravel/framework": "^11.31",
"laravel/framework": "^12.0",
"laravel/reverb": "^1.0",
"laravel/sanctum": "^4.0",
"laravel/telescope": "^5.5",
"laravel/tinker": "^2.9",

1367
composer.lock generated

File diff suppressed because it is too large Load Diff

82
config/broadcasting.php Normal file
View File

@ -0,0 +1,82 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
*/
'default' => env('BROADCAST_CONNECTION', 'reverb'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over WebSockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];

View File

@ -4,5 +4,7 @@ return [
"driver" => [
"url" => $_ENV['DUSK_DRIVER_URL'] ?? env('DUSK_DRIVER_URL') ?? null
]
],
"shouldStartMaximized" => $_ENV['DUSK_START_MAXIMIZED'] ?? env('DUSK_START_MAXIMIZED') ?? false,
"headlessDisabled" => $_ENV['DUSK_HEADLESS_DISABLED'] ?? env('DUSK_HEADLESS_DISABLED') ?? false
];

View File

@ -75,6 +75,10 @@ return [
'links' => [
public_path('storage') => storage_path('app/public'),
public_path('console') => base_path('app/Browser/console'),
public_path('downloads') => base_path('app/Browser/downloads'),
public_path('screenshots') => base_path('app/Browser/screenshots'),
public_path('source') => base_path('app/Browser/source'),
],
];

20
config/jobs.php Normal file
View File

@ -0,0 +1,20 @@
<?php
use Laravel\Telescope\Http\Middleware\Authorize;
use Laravel\Telescope\Watchers;
return [
/**
* Remove old job runs from the database.
*/
'pruneOldJobRuns' => [
'enabled' => true,
/**
* How many job runs a job can keep before we start pruning old ones.
*/
'max_runs_per_job' => 50,
],
];

92
config/reverb.php Normal file
View File

@ -0,0 +1,92 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Reverb Server
|--------------------------------------------------------------------------
|
| This option controls the default server used by Reverb to handle
| incoming messages as well as broadcasting message to all your
| connected clients. At this time only "reverb" is supported.
|
*/
'default' => env('REVERB_SERVER', 'reverb'),
/*
|--------------------------------------------------------------------------
| Reverb Servers
|--------------------------------------------------------------------------
|
| Here you may define details for each of the supported Reverb servers.
| Each server has its own configuration options that are defined in
| the array below. You should ensure all the options are present.
|
*/
'servers' => [
'reverb' => [
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
'port' => env('REVERB_SERVER_PORT', 8080),
'hostname' => env('REVERB_HOST'),
'options' => [
'tls' => [],
],
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
'scaling' => [
'enabled' => env('REVERB_SCALING_ENABLED', false),
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
'server' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DB', '0'),
],
],
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
],
],
/*
|--------------------------------------------------------------------------
| Reverb Applications
|--------------------------------------------------------------------------
|
| Here you may define how Reverb applications are managed. If you choose
| to use the "config" provider, you may define an array of apps which
| your server will support, including their connection credentials.
|
*/
'apps' => [
'provider' => 'config',
'apps' => [
[
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'allowed_origins' => ['*'],
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
],
],
],
];

View File

@ -0,0 +1,82 @@
<?php
use App\Models\Job;
use App\Models\JobInfo;
use App\Models\JobInfoType;
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
{
$newJobId = 3;
Job::forceCreate([
"id" => $newJobId,
"name" => "Hellcase Battles",
"description" => "Envoie les meilleures battles d'Hellcase",
]);
JobInfo::forceCreate([
"key" => "hellcase_battles_discord_webhook_url",
"name" => "Webhook Discord",
"description" => "Le lien discord webhook utilisé pour envoyer les meilleures battles d'Hellcase.\nSi aucun n'est spécifié, le webhook Discord des paramètres généraux sera utilisé.",
"placeholder" => "https://discord.com/api/webhooks/...",
"is_required" => false,
"job_info_type_id" => 4,
"job_id" => $newJobId,
]);
JobInfoType::forceCreate([
"id" => 5,
"name" => "number",
]);
JobInfoType::forceCreate([
"id" => 6,
"name" => "boolean",
]);
JobInfo::forceCreate([
"key" => "hellcase_battles_minimum_value",
"name" => "Valeur minimum des battles",
"description" => "La valeur minimale qu'une battle doit avoir pour être envoyée, en euros.",
"placeholder" => "1000",
"job_info_type_id" => 5,
"job_id" => $newJobId,
]);
JobInfo::forceCreate([
"key" => "hellcase_battles_allow_bots",
"name" => "Autoriser les battles avec bots",
"description" => "Envoyer les battles avec un seul joueur et des bots.",
"is_required" => false,
"job_info_type_id" => 6,
"job_id" => $newJobId,
]);
Schema::create('hellcase_battles', function (Blueprint $table) {
$table->id();
$table->string("battle_id")->unique();
$table->float("value");
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Job::where("id", 3)->delete();
JobInfo::where("job_id", 3)->delete();
JobInfoType::whereIn("id", [5, 6])->delete();
Schema::dropIfExists('hellcase_battles');
}
};

77
package-lock.json generated
View File

@ -11,6 +11,7 @@
"lucide-react": "^0.474.0",
"lucide-vue-next": "^0.474.0",
"radix-vue": "^1.9.13",
"reka-ui": "^2.1.0",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
},
@ -21,8 +22,10 @@
"autoprefixer": "^10.4.12",
"axios": "^1.7.4",
"concurrently": "^9.0.1",
"laravel-echo": "^2.0.2",
"laravel-vite-plugin": "^1.2.0",
"postcss": "^8.4.31",
"pusher-js": "^8.4.0",
"sass-embedded": "^1.83.4",
"tailwindcss": "^3.2.1",
"typescript": "^5.6.3",
@ -956,20 +959,20 @@
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.3.tgz",
"integrity": "sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w==",
"version": "3.13.4",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.4.tgz",
"integrity": "sha512-fNGO9fjjSLns87tlcto106enQQLycCKR4DPNpgq3djP5IdcPFdPAmaKjsgzIeRhH7hWrELgW12hYnRthS5kLUw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/vue-virtual": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.11.3.tgz",
"integrity": "sha512-BVZ00i5XBucetRj2doVd32jOPtJthvZSVJvx9GL4gSQsyngliSCtzlP1Op7TFrEtmebRKT8QUQE1tRhOQzWecQ==",
"version": "3.13.4",
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.4.tgz",
"integrity": "sha512-1fPrd3hE1SS4R/9JbX1AlzueY4duCK7ixuLcMW5GMnk9N6WbLo9MioNKiv22V+UaXKOLNy8tLdzT8NYerOFTOQ==",
"dependencies": {
"@tanstack/virtual-core": "3.11.3"
"@tanstack/virtual-core": "3.13.4"
},
"funding": {
"type": "github",
@ -2239,6 +2242,15 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/laravel-echo": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.0.2.tgz",
"integrity": "sha512-Ciai6hA7r35MFqNRb8G034cvm9WiveSTFQQKRGJhWtZGbng7C8BBa5QvqDxk/Mw5GeJ+q19jrEwQhf7r1b1lcg==",
"dev": true,
"engines": {
"node": ">=20"
}
},
"node_modules/laravel-vite-plugin": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.2.0.tgz",
@ -2487,6 +2499,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ohash": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.6.tgz",
"integrity": "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@ -2700,6 +2717,15 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"node_modules/pusher-js": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz",
"integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==",
"dev": true,
"dependencies": {
"tweetnacl": "^1.0.3"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@ -2847,6 +2873,37 @@
"node": ">=8.10.0"
}
},
"node_modules/reka-ui": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.1.0.tgz",
"integrity": "sha512-w4kEDEyXhIqv4QeFJeiuBc4mQP37hH/UTRpEb9dMbPdR49JG5TcV/s0+ntNRONUUW4LDLX7E1ZPcwBw5hnu0yw==",
"dependencies": {
"@floating-ui/dom": "^1.6.13",
"@floating-ui/vue": "^1.1.6",
"@internationalized/date": "^3.5.0",
"@internationalized/number": "^3.5.0",
"@tanstack/vue-virtual": "^3.12.0",
"@vueuse/core": "^12.5.0",
"@vueuse/shared": "^12.5.0",
"aria-hidden": "^1.2.4",
"defu": "^6.1.4",
"ohash": "^1.1.4"
},
"peerDependencies": {
"vue": ">= 3.2.0"
}
},
"node_modules/reka-ui/node_modules/@vueuse/shared": {
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz",
"integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==",
"dependencies": {
"vue": "^3.5.13"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -3697,6 +3754,12 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"dev": true
},
"node_modules/typescript": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",

View File

@ -12,8 +12,10 @@
"autoprefixer": "^10.4.12",
"axios": "^1.7.4",
"concurrently": "^9.0.1",
"laravel-echo": "^2.0.2",
"laravel-vite-plugin": "^1.2.0",
"postcss": "^8.4.31",
"pusher-js": "^8.4.0",
"sass-embedded": "^1.83.4",
"tailwindcss": "^3.2.1",
"typescript": "^5.6.3",
@ -22,12 +24,14 @@
"vue-tsc": "^2.0.24"
},
"dependencies": {
"@tanstack/vue-table": "^8.21.2",
"@vueuse/core": "^12.5.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.474.0",
"lucide-vue-next": "^0.474.0",
"radix-vue": "^1.9.13",
"reka-ui": "^2.1.0",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
}

281
pnpm-lock.yaml generated
View File

@ -8,6 +8,12 @@ importers:
.:
dependencies:
'@tanstack/vue-table':
specifier: ^8.21.2
version: 8.21.2(vue@3.5.13(typescript@5.7.3))
'@vueuse/core':
specifier: ^12.5.0
version: 12.7.0(typescript@5.7.3)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@ -17,6 +23,15 @@ importers:
lucide-react:
specifier: ^0.474.0
version: 0.474.0(react@19.0.0)
lucide-vue-next:
specifier: ^0.474.0
version: 0.474.0(vue@3.5.13(typescript@5.7.3))
radix-vue:
specifier: ^1.9.13
version: 1.9.16(vue@3.5.13(typescript@5.7.3))
reka-ui:
specifier: ^2.1.0
version: 2.1.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
tailwind-merge:
specifier: ^2.6.0
version: 2.6.0
@ -42,12 +57,18 @@ importers:
concurrently:
specifier: ^9.0.1
version: 9.1.2
laravel-echo:
specifier: ^2.0.2
version: 2.0.2
laravel-vite-plugin:
specifier: ^1.2.0
version: 1.2.0(vite@6.0.11(jiti@1.21.7)(sass-embedded@1.83.4)(yaml@2.7.0))
postcss:
specifier: ^8.4.31
version: 8.5.1
pusher-js:
specifier: ^8.4.0
version: 8.4.0
sass-embedded:
specifier: ^1.83.4
version: 1.83.4
@ -243,6 +264,18 @@ packages:
cpu: [x64]
os: [win32]
'@floating-ui/core@1.6.9':
resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==}
'@floating-ui/dom@1.6.13':
resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
'@floating-ui/vue@1.1.6':
resolution: {integrity: sha512-XFlUzGHGv12zbgHNk5FN2mUB7ROul3oG2ENdTpWdE+qMFxyNxWSRmsoyhiEnpmabNm6WnUvR1OvJfUfN4ojC1A==}
'@inertiajs/core@2.0.3':
resolution: {integrity: sha512-JvXzqc2XAt3WgEDMyxCyXO6bDLMCsBjFsYREU1/+3wtNTib7QKwK71+aF+MrhILpz+kRTi29TsLqnbkPHBAZjw==}
@ -251,6 +284,12 @@ packages:
peerDependencies:
vue: ^3.0.0
'@internationalized/date@3.7.0':
resolution: {integrity: sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ==}
'@internationalized/number@3.6.0':
resolution: {integrity: sha512-PtrRcJVy7nw++wn4W2OuePQQfTqDzfusSuY1QTtui4wa7r+rGVtR75pO8CyKvHvzyQYi3Q1uO5sY0AsB4e65Bw==}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@ -384,14 +423,38 @@ packages:
cpu: [x64]
os: [win32]
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tailwindcss/forms@0.5.10':
resolution: {integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==}
peerDependencies:
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1'
'@tanstack/table-core@8.21.2':
resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==}
engines: {node: '>=12'}
'@tanstack/virtual-core@3.13.2':
resolution: {integrity: sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==}
'@tanstack/vue-table@8.21.2':
resolution: {integrity: sha512-KBgOWxha/x4m1EdhVWxOpqHb661UjqAxzPcmXR3QiA7aShZ547x19Gw0UJX9we+m+tVcPuLRZ61JsYW47QZFfQ==}
engines: {node: '>=12'}
peerDependencies:
vue: '>=3.2'
'@tanstack/vue-virtual@3.13.2':
resolution: {integrity: sha512-z4swzjdhzCh95n9dw9lTvw+t3iwSkYRlVkYkra3C9mul/m5fTzHR7KmtkwH4qXMTXGJUbngtC/bz2cHQIHkO8g==}
peerDependencies:
vue: ^2.7.0 || ^3.0.0
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
'@types/web-bluetooth@0.0.20':
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
'@vitejs/plugin-vue@5.2.1':
resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==}
engines: {node: ^18.0.0 || >=20.0.0}
@ -448,6 +511,24 @@ packages:
'@vue/shared@3.5.13':
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
'@vueuse/core@10.11.1':
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
'@vueuse/core@12.7.0':
resolution: {integrity: sha512-jtK5B7YjZXmkGNHjviyGO4s3ZtEhbzSgrbX+s5o+Lr8i2nYqNyHuPVOeTdM1/hZ5Tkxg/KktAuAVDDiHMraMVA==}
'@vueuse/metadata@10.11.1':
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
'@vueuse/metadata@12.7.0':
resolution: {integrity: sha512-4VvTH9mrjXqFN5LYa5YfqHVRI6j7R00Vy4995Rw7PQxyCL3z0Lli86iN4UemWqixxEvYfRjG+hF9wL8oLOn+3g==}
'@vueuse/shared@10.11.1':
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
'@vueuse/shared@12.7.0':
resolution: {integrity: sha512-coLlUw2HHKsm7rPN6WqHJQr18WymN4wkA/3ThFaJ4v4gWGWAQQGK+MJxLuJTBs4mojQiazlVWAKNJNpUWGRkNw==}
alien-signals@0.4.14:
resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==}
@ -477,6 +558,10 @@ packages:
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
aria-hidden@1.2.4:
resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
engines: {node: '>=10'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@ -588,6 +673,9 @@ packages:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
@ -642,6 +730,9 @@ packages:
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-glob@3.3.3:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
@ -762,6 +853,10 @@ packages:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
laravel-echo@2.0.2:
resolution: {integrity: sha512-Ciai6hA7r35MFqNRb8G034cvm9WiveSTFQQKRGJhWtZGbng7C8BBa5QvqDxk/Mw5GeJ+q19jrEwQhf7r1b1lcg==}
engines: {node: '>=20'}
laravel-vite-plugin@1.2.0:
resolution: {integrity: sha512-R0pJ+IcTVeqEMoKz/B2Ij57QVq3sFTABiFmb06gAwFdivbOgsUtuhX6N2MGLEArajrS3U5JbberzwOe7uXHMHQ==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -794,6 +889,11 @@ packages:
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
lucide-vue-next@0.474.0:
resolution: {integrity: sha512-bQaSBjfJ33xiPQCxCf4JD3rcUgZFgWZzxSY8SScNa4Mcq2vWGlbvQx6icTL1UXRqsxzfoT13RXawePSmgg4iWw==}
peerDependencies:
vue: '>=3.0.1'
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
@ -840,6 +940,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@5.1.2:
resolution: {integrity: sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==}
engines: {node: ^18 || >=20}
hasBin: true
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
@ -863,6 +968,9 @@ packages:
resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
engines: {node: '>= 0.4'}
ohash@1.1.6:
resolution: {integrity: sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
@ -939,6 +1047,9 @@ packages:
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
pusher-js@8.4.0:
resolution: {integrity: sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==}
qs@6.14.0:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
@ -946,6 +1057,11 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
radix-vue@1.9.16:
resolution: {integrity: sha512-xwkTfQ7Ub/0XmT40JDc3g03xuYqKIJzVKGazcIkk8mUksj/tbw1pcCVRP0e3hKvPHKeQ0cktI1MvRnlUwCRvoQ==}
peerDependencies:
vue: '>= 3.2.0'
react@19.0.0:
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
engines: {node: '>=0.10.0'}
@ -957,6 +1073,11 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
reka-ui@2.1.0:
resolution: {integrity: sha512-w4kEDEyXhIqv4QeFJeiuBc4mQP37hH/UTRpEb9dMbPdR49JG5TcV/s0+ntNRONUUW4LDLX7E1ZPcwBw5hnu0yw==}
peerDependencies:
vue: '>= 3.2.0'
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@ -1217,6 +1338,9 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tweetnacl@1.0.3:
resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==}
typescript@5.7.3:
resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
engines: {node: '>=14.17'}
@ -1280,6 +1404,17 @@ packages:
vscode-uri@3.0.8:
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
engines: {node: '>=12'}
hasBin: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue-tsc@2.2.0:
resolution: {integrity: sha512-gtmM1sUuJ8aSb0KoAFmK9yMxb8TxjewmxqTJ1aKphD5Cbu0rULFY6+UQT51zW7SpUcenfPUuflKyVwyx9Qdnxg==}
hasBin: true
@ -1418,6 +1553,26 @@ snapshots:
'@esbuild/win32-x64@0.24.2':
optional: true
'@floating-ui/core@1.6.9':
dependencies:
'@floating-ui/utils': 0.2.9
'@floating-ui/dom@1.6.13':
dependencies:
'@floating-ui/core': 1.6.9
'@floating-ui/utils': 0.2.9
'@floating-ui/utils@0.2.9': {}
'@floating-ui/vue@1.1.6(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@floating-ui/dom': 1.6.13
'@floating-ui/utils': 0.2.9
vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@inertiajs/core@2.0.3':
dependencies:
axios: 1.7.9
@ -1435,6 +1590,14 @@ snapshots:
transitivePeerDependencies:
- debug
'@internationalized/date@3.7.0':
dependencies:
'@swc/helpers': 0.5.15
'@internationalized/number@3.6.0':
dependencies:
'@swc/helpers': 0.5.15
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@ -1533,13 +1696,33 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.32.1':
optional: true
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
'@tailwindcss/forms@0.5.10(tailwindcss@3.4.17)':
dependencies:
mini-svg-data-uri: 1.4.4
tailwindcss: 3.4.17
'@tanstack/table-core@8.21.2': {}
'@tanstack/virtual-core@3.13.2': {}
'@tanstack/vue-table@8.21.2(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@tanstack/table-core': 8.21.2
vue: 3.5.13(typescript@5.7.3)
'@tanstack/vue-virtual@3.13.2(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@tanstack/virtual-core': 3.13.2
vue: 3.5.13(typescript@5.7.3)
'@types/estree@1.0.6': {}
'@types/web-bluetooth@0.0.20': {}
'@vitejs/plugin-vue@5.2.1(vite@6.0.11(jiti@1.21.7)(sass-embedded@1.83.4)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))':
dependencies:
vite: 6.0.11(jiti@1.21.7)(sass-embedded@1.83.4)(yaml@2.7.0)
@ -1629,6 +1812,42 @@ snapshots:
'@vue/shared@3.5.13': {}
'@vueuse/core@10.11.1(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@types/web-bluetooth': 0.0.20
'@vueuse/metadata': 10.11.1
'@vueuse/shared': 10.11.1(vue@3.5.13(typescript@5.7.3))
vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/core@12.7.0(typescript@5.7.3)':
dependencies:
'@types/web-bluetooth': 0.0.20
'@vueuse/metadata': 12.7.0
'@vueuse/shared': 12.7.0(typescript@5.7.3)
vue: 3.5.13(typescript@5.7.3)
transitivePeerDependencies:
- typescript
'@vueuse/metadata@10.11.1': {}
'@vueuse/metadata@12.7.0': {}
'@vueuse/shared@10.11.1(vue@3.5.13(typescript@5.7.3))':
dependencies:
vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/shared@12.7.0(typescript@5.7.3)':
dependencies:
vue: 3.5.13(typescript@5.7.3)
transitivePeerDependencies:
- typescript
alien-signals@0.4.14: {}
ansi-regex@5.0.1: {}
@ -1650,6 +1869,10 @@ snapshots:
arg@5.0.2: {}
aria-hidden@1.2.4:
dependencies:
tslib: 2.8.1
asynckit@0.4.0: {}
autoprefixer@10.4.20(postcss@8.5.1):
@ -1772,6 +1995,8 @@ snapshots:
deepmerge@4.3.1: {}
defu@6.1.4: {}
delayed-stream@1.0.0: {}
didyoumean@1.2.2: {}
@ -1834,6 +2059,8 @@ snapshots:
estree-walker@2.0.2: {}
fast-deep-equal@3.1.3: {}
fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -1949,6 +2176,8 @@ snapshots:
jiti@1.21.7: {}
laravel-echo@2.0.2: {}
laravel-vite-plugin@1.2.0(vite@6.0.11(jiti@1.21.7)(sass-embedded@1.83.4)(yaml@2.7.0)):
dependencies:
picocolors: 1.1.1
@ -1971,6 +2200,10 @@ snapshots:
dependencies:
react: 19.0.0
lucide-vue-next@0.474.0(vue@3.5.13(typescript@5.7.3)):
dependencies:
vue: 3.5.13(typescript@5.7.3)
magic-string@0.30.17:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
@ -2008,6 +2241,8 @@ snapshots:
nanoid@3.3.8: {}
nanoid@5.1.2: {}
node-releases@2.0.19: {}
normalize-path@3.0.0: {}
@ -2020,6 +2255,8 @@ snapshots:
object-inspect@1.13.3: {}
ohash@1.1.6: {}
package-json-from-dist@1.0.1: {}
path-browserify@1.0.1: {}
@ -2080,12 +2317,33 @@ snapshots:
proxy-from-env@1.1.0: {}
pusher-js@8.4.0:
dependencies:
tweetnacl: 1.0.3
qs@6.14.0:
dependencies:
side-channel: 1.1.0
queue-microtask@1.2.3: {}
radix-vue@1.9.16(vue@3.5.13(typescript@5.7.3)):
dependencies:
'@floating-ui/dom': 1.6.13
'@floating-ui/vue': 1.1.6(vue@3.5.13(typescript@5.7.3))
'@internationalized/date': 3.7.0
'@internationalized/number': 3.6.0
'@tanstack/vue-virtual': 3.13.2(vue@3.5.13(typescript@5.7.3))
'@vueuse/core': 10.11.1(vue@3.5.13(typescript@5.7.3))
'@vueuse/shared': 10.11.1(vue@3.5.13(typescript@5.7.3))
aria-hidden: 1.2.4
defu: 6.1.4
fast-deep-equal: 3.1.3
nanoid: 5.1.2
vue: 3.5.13(typescript@5.7.3)
transitivePeerDependencies:
- '@vue/composition-api'
react@19.0.0: {}
read-cache@1.0.0:
@ -2096,6 +2354,23 @@ snapshots:
dependencies:
picomatch: 2.3.1
reka-ui@2.1.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)):
dependencies:
'@floating-ui/dom': 1.6.13
'@floating-ui/vue': 1.1.6(vue@3.5.13(typescript@5.7.3))
'@internationalized/date': 3.7.0
'@internationalized/number': 3.6.0
'@tanstack/vue-virtual': 3.13.2(vue@3.5.13(typescript@5.7.3))
'@vueuse/core': 12.7.0(typescript@5.7.3)
'@vueuse/shared': 12.7.0(typescript@5.7.3)
aria-hidden: 1.2.4
defu: 6.1.4
ohash: 1.1.6
vue: 3.5.13(typescript@5.7.3)
transitivePeerDependencies:
- '@vue/composition-api'
- typescript
require-directory@2.1.1: {}
resolve@1.22.10:
@ -2368,6 +2643,8 @@ snapshots:
tslib@2.8.1: {}
tweetnacl@1.0.3: {}
typescript@5.7.3: {}
update-browserslist-db@1.1.2(browserslist@4.24.4):
@ -2398,6 +2675,10 @@ snapshots:
vscode-uri@3.0.8: {}
vue-demi@0.14.10(vue@3.5.13(typescript@5.7.3)):
dependencies:
vue: 3.5.13(typescript@5.7.3)
vue-tsc@2.2.0(typescript@5.7.3):
dependencies:
'@volar/typescript': 2.4.11

1
public/console Symbolic link
View File

@ -0,0 +1 @@
/home/ninluc/Documents/codage/DatBrowser/app/Browser/console

1
public/downloads Symbolic link
View File

@ -0,0 +1 @@
/home/ninluc/Documents/codage/DatBrowser/app/Browser/downloads

1
public/screenshots Symbolic link
View File

@ -0,0 +1 @@
/home/ninluc/Documents/codage/DatBrowser/app/Browser/screenshots

1
public/source Symbolic link
View File

@ -0,0 +1 @@
/home/ninluc/Documents/codage/DatBrowser/app/Browser/source

View File

@ -18,7 +18,7 @@ const jobInfoType = props.jobInfo.job_info_type.name;
<div>
<Label :for="'' + jobInfo.id" class="text">{{ jobInfo.name }}<span v-if="jobInfo.is_required" class="cursor-help" title="Requis" aria-label="Requis">*</span></Label>
<Description>{{ jobInfo.description }}</Description>
<Input v-if="jobInfoType != 'checkbox'" :type="jobInfoType" :id="'' + jobInfo.id" :name="'' + jobInfo.id" :placeholder="jobInfo.placeholder" v-model="jobInfo.value as string" :required="jobInfo.is_required" />
<Input v-if="['text', 'email', 'password', 'url', 'number'].includes(jobInfoType)" :type="jobInfoType" :id="'' + jobInfo.id" :name="'' + jobInfo.id" :placeholder="jobInfo.placeholder" v-model="jobInfo.value as string" :required="jobInfo.is_required" />
<VModelCheckbox v-else :id="'' + jobInfo.id" :class="''" v-model="jobInfo.value as boolean" />
</div>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import Separator from "@/Components/ui/separator/Separator.vue";
import { JobRunArtifact } from "@/types/Jobs/job";
defineProps<{
jobRun: JobRunArtifact;
}>();
</script>
<template>
<ul>
<li v-for="artifact in jobRun.artifacts" :key="artifact.id">
<p>{{ artifact.name }}</p>
<p class="italic">{{ artifact.content }}</p>
</li>
</ul>
</template>
<style lang="scss" scoped>
ul {
list-style-type: circle;
padding-left: 1rem;
}
</style>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import AccordionContent from "@/Components/ui/accordion/AccordionContent.vue";
import AccordionItem from "@/Components/ui/accordion/AccordionItem.vue";
import AccordionTrigger from "@/Components/ui/accordion/AccordionTrigger.vue";
import { JobRunArtifact } from "@/types/Jobs/job";
import JobRunArtifacts from "./JobRunArtifacts.vue";
defineProps<{
jobRun: JobRunArtifact;
}>();
</script>
<template>
<AccordionItem :value="''+jobRun.id" :class="[jobRun.success ? 'bg-green-100' : 'bg-red-200', 'first:rounded-t last:rounded-b', 'px-3']">
<AccordionTrigger>
{{ new Date(Date.parse(jobRun.created_at)).toLocaleTimeString(undefined, {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})
}}
</AccordionTrigger>
<AccordionContent>
<JobRunArtifacts :jobRun="jobRun" />
</AccordionContent>
</AccordionItem>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { Job, JobRunArtifact } from "@/types/Jobs/job";
import JobRunItem from "./JobRunItem.vue";
import Accordion from "@/Components/ui/accordion/Accordion.vue";
import ScrollArea from "@/Components/ui/scroll-area/ScrollArea.vue";
defineProps<{
job: Job;
}>();
</script>
<template>
<div v-if="job.job_runs.length > 0">
<h2>Ancien jobs</h2>
<ScrollArea class="min-h-[300px] max-h-[20vh] overflow-auto pr-2">
<Accordion type="multiple" collapsible>
<JobRunItem
:jobRun="jobRun"
v-for="jobRun in job.job_runs.sort((a, b) => {
return (
new Date(b.created_at).getTime() -
new Date(a.created_at).getTime()
);
})"
:key="jobRun.id"
/>
</Accordion>
</ScrollArea>
</div>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import {
AccordionRoot,
type AccordionRootEmits,
type AccordionRootProps,
useForwardPropsEmits,
} from 'reka-ui'
const props = defineProps<AccordionRootProps>()
const emits = defineEmits<AccordionRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<AccordionRoot v-bind="forwarded">
<slot />
</AccordionRoot>
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { AccordionContent, type AccordionContentProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<AccordionContent
v-bind="delegatedProps"
class="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
>
<div :class="cn('pb-4 pt-0', props.class)">
<slot />
</div>
</AccordionContent>
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<AccordionItem
v-bind="forwardedProps"
:class="cn('border-b', props.class)"
>
<slot />
</AccordionItem>
</template>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { ChevronDown } from 'lucide-vue-next'
import {
AccordionHeader,
AccordionTrigger,
type AccordionTriggerProps,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<AccordionHeader class="flex">
<AccordionTrigger
v-bind="delegatedProps"
:class="
cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
props.class,
)
"
>
<slot />
<slot name="icon">
<ChevronDown
class="h-4 w-4 shrink-0 transition-transform duration-200"
/>
</slot>
</AccordionTrigger>
</AccordionHeader>
</template>

View File

@ -0,0 +1,4 @@
export { default as Accordion } from './Accordion.vue'
export { default as AccordionContent } from './AccordionContent.vue'
export { default as AccordionItem } from './AccordionItem.vue'
export { default as AccordionTrigger } from './AccordionTrigger.vue'

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { Separator, type SeparatorProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<
SeparatorProps & { class?: HTMLAttributes['class'], label?: string }
>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<Separator
v-bind="delegatedProps"
:class="
cn(
'shrink-0 bg-border relative',
props.orientation === 'vertical' ? 'w-px h-full' : 'h-px w-full',
props.class,
)
"
>
<span
v-if="props.label"
:class="cn('text-xs text-muted-foreground bg-background absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex justify-center items-center',
props.orientation === 'vertical' ? 'w-[1px] px-1 py-2' : 'h-[1px] py-1 px-2',
)"
>{{ props.label }}</span>
</Separator>
</template>

View File

@ -0,0 +1 @@
export { default as Separator } from './Separator.vue'

View File

@ -11,7 +11,7 @@ const jobs = ref<Job[]>([]);
async function fetchJobs() {
let jobsRaw = await httpApi<Job[]>("/jobs");
jobs.value = jobsRaw.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
jobs.value = jobsRaw.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
}
onMounted(fetchJobs);

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import JobForm from '../Components/Layout/Job/JobForm.vue'
import JobCard from '../Components/Layout/Job/JobCard.vue'
import JobRuns from '../Components/Layout/Job/JobRuns/JobRuns.vue';
import { Job } from "@/types/Jobs/job";
import { Head } from "@inertiajs/vue3";
@ -16,4 +17,6 @@ defineProps<{
<JobCard :job="job" />
<JobForm :job="job" :error="error" />
<JobRuns :job="job" />
</template>

14
resources/js/echo.js Normal file
View File

@ -0,0 +1,14 @@
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});

View File

@ -1,8 +1,17 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import type { Updater } from '@tanstack/vue-table'
import type { Ref } from 'vue'
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
return twMerge(clsx(inputs))
}
export function valueUpdater<T extends Updater<any>>(updaterOrValue: T, ref: Ref) {
ref.value
= typeof updaterOrValue === 'function'
? updaterOrValue(ref.value)
: updaterOrValue
}
export async function httpApi<T>(route: string): Promise<T> {

View File

@ -5,8 +5,9 @@ export type Job = {
is_active: boolean;
job_infos: JobInfo[];
job_runs: JobRunArtifact[];
created_at: Date;
created_at: string;
}
export type JobInfo = {
@ -26,16 +27,23 @@ export type JobInfo = {
export type JobInfoType = {
id: number;
name: string;
created_at: Date;
created_at: string;
}
export type JobRunArtifact = {
jobId: number;
id: number;
job_id: number;
artifacts: JobArtifact[];
success: boolean;
created_at: string;
}
export type JobArtifact = {
id: number;
name: string;
content: string;
created_at: string;
}

View File

@ -12,6 +12,7 @@ Route::get('/jobs', function (Request $request) {
Route::get('/test/{id}', function (Request $request, $id, BrowserJobsInstances $BrowserJobsInstances) {
$log = $BrowserJobsInstances->getJobInstance($id)->execute();
dump($log);
return response()->json(['message' => 'Job ' . $id . ' ran', 'jobRun' => $log->load('artifacts')]);
});

4
routes/channels.php Normal file
View File

@ -0,0 +1,4 @@
<?php
use Illuminate\Support\Facades\Broadcast;

View File

@ -1,7 +1,8 @@
<?php
use App\Browser\Jobs\Hellcase\HellcaseJob;
use App\Models\Job;
use App\Browser\Jobs\HellcaseBattles\HellcaseBattlesJob;
use App\Jobs\PruneOldJobRuns;
use App\Services\BrowserJobsInstances;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
@ -13,6 +14,12 @@ Artisan::command('inspire', function () {
// Telescope
Schedule::command('telescope:prune')->monthly();
// Prune old job runs
Schedule::job(new PruneOldJobRuns)->monthly()->onOneServer()->withoutOverlapping()->name('prune-old-job-runs')->description('Prune old job runs')->skip(function () {
return !config('jobs.pruneOldJobRuns.enabled');
});
// Jobs
Schedule::job(new HellcaseJob)->daily()->onOneServer()->withoutOverlapping()->name('hellcase')->description('Hellcase job');
// Schedule::job(new HellcaseJob)->everyMinute()->onOneServer()->withoutOverlapping()->name('hellcase')->description('Hellcase job');
Schedule::job(new HellcaseBattlesJob)->hourly()->onOneServer()->withoutOverlapping()->name('hellcase_battles')->description('Hellcase battles job');

23
todo.md Normal file
View File

@ -0,0 +1,23 @@
# TODO
- Fix hellcase, fermer lespopups à chaque nouvelle visite
- Voir si le scheduler fonctionne au démmarage
- Mettre un timeout pour pas overwhelm le pc au démmarage
- Image pour le join de giveaway
- → Notification
- Ou ajouter des images base64 dans les jobRun
- Risque de devenir volumineux
- Sauf avec le job qui enlève les vieilles jobRun
- Notification live websocket
- Websocket installé
- Serveur php plus propre (nginx, apache, n'importe)
- Epic games
Pas l'air possible avec cloudflare
- Petit bug, quand l'on enregistre un formulaire avec une erreur, l'url a un argument GET ?error=mon%24erreur
Du coup dans la nav le job actuel n'est plus reconnu
## Pour deploy Lama
- Version Lama du compose
- Tuto installation

Binary file not shown.

View File

@ -1 +1,20 @@
sudo docker run --rm -it -p 3389:3389 -v ./undetectedChromedriver:/root/.local/share/undetected_chromedriver/ ultrafunk/undetected-chromedriver:latest
#!/bin/bash
# From undetected chromedriver docker
#sudo docker run --rm -it -p 3389:3389 -v ./undetectedChromedriver:/root/.local/share/undetected_chromedriver/ ultrafunk/undetected-chromedriver:latest
# With undetected chromedriver patcher
# Run the selenium/standalone-chrome:latest with a specific container name in the background
sudo docker run -d --name standalone-chrome selenium/standalone-chrome:latest
sleep 5
# Copy the chromedriver binary from the container to the host
sudo docker cp -L standalone-chrome:/bin/chromedriver ./chromedriver
# Stop the container
sudo docker stop standalone-chrome
sudo chmod 777 ./chromedriver
# Patch the chromedriver binary
python3 ./patchChromedriver.py

View File

@ -0,0 +1,8 @@
#!/bin/python3
import undetected_chromedriver as uc
options = uc.ChromeOptions()
# Chromedriver is in current directory
driver = uc.Chrome(options = options, browser_executable_path="/usr/bin/google-chrome", driver_executable_path="/home/ninluc/Documents/codage/DatBrowser/undetectedChromedriver/chromedriver")
driver.get('https://nowsecure.nl')

View File

@ -1,6 +1,7 @@
FROM selenium/standalone-chrome:108.0 AS final
# FROM selenium/standalone-chrome:108.0 AS final
FROM selenium/standalone-chrome:latest AS final
COPY undetectedChromedriver/chromedriver-linux /bin/chromedriver
COPY undetectedChromedriver/chromedriver /bin/chromedriver
RUN mkdir -p /home/seluser/profile/
ENV TZ=Europe/Brussels