[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[*.{yml,yaml}]\nindent_size = 2\n\n[docker-compose.yml]\nindent_size = 4\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n\n*.blade.php diff=html\n*.css diff=css\n*.html diff=html\n*.md diff=markdown\n*.php diff=php\n\n/.github export-ignore\nCHANGELOG.md export-ignore\n.styleci.yml export-ignore\n"
  },
  {
    "path": ".github/workflows/code-style.yml",
    "content": "name: PHP Linting\non:\n  pull_request:\n    branches:\n      - 'feature/*'\n  push:\n    branches:\n      - 'main'\n\njobs:\n  phplint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: \"laravel-pint\"\n        uses: aglipanci/laravel-pint-action@2.0.0\n        with:\n          preset: laravel\n          pintVersion: 1.21.2\n"
  },
  {
    "path": ".github/workflows/image.yml",
    "content": "name: Build and publish image\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - 'develop'\n      - 'main'\n\njobs:\n  build-and-push-image:\n    if: ${{ github.event_name != 'pull_request' }}\n    runs-on: ubuntu-latest\n    permissions:\n      packages: write\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@v3\n        with:\n          registry: https://ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Set image tag\n        id: tag\n        shell: bash\n        run: |\n          if [ \"${GITHUB_REF}\" = \"refs/heads/main\" ]; then\n            echo \"tag=latest\" >> \"$GITHUB_OUTPUT\"\n          else\n            echo \"tag=dev\" >> \"$GITHUB_OUTPUT\"\n          fi\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v5\n        with:\n          push: true\n          tags: ghcr.io/govigilant/vigilant:${{ steps.tag.outputs.tag }}\n\n"
  },
  {
    "path": ".github/workflows/package-quality.yml",
    "content": "name: Package quality\n\non:\n  push:\n    branches: [main]\n  pull_request:\n\njobs:\n  get-packages:\n    runs-on: ubuntu-latest\n    outputs:\n      matrix: ${{ steps.set-matrix.outputs.matrix }}\n    steps:\n      - uses: actions/checkout@v3\n\n      - id: set-matrix\n        run: |\n          packages=$(ls -d packages/*/ | sed 's|packages/||; s|/||' | jq -R -s -c 'split(\"\\n\")[:-1]')\n          echo \"Detected packages: $packages\"\n          echo \"matrix=$packages\" >> $GITHUB_OUTPUT\n\n  quality:\n    needs: get-packages\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        package: ${{ fromJson(needs.get-packages.outputs.matrix) }}\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: '8.4'\n          extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo\n          coverage: none\n\n      - name: Run quality check script\n        run: |\n          chmod +x ./scripts/package-quality.sh\n          ./scripts/package-quality.sh \"${{ matrix.package }}\"\n        shell: bash\n"
  },
  {
    "path": ".github/workflows/release-image.yml",
    "content": "name: Build and publish release image\n\non:\n  release:\n    types: [published]\n\njobs:\n  build-and-push-release-image:\n    runs-on: ubuntu-latest\n    permissions:\n      packages: write\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@v3\n        with:\n          registry: https://ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Build and push Docker image with release tag\n        uses: docker/build-push-action@v5\n        with:\n          push: true\n          tags: ghcr.io/govigilant/vigilant:${{ github.event.release.tag_name }}\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\n\non: ['push', 'pull_request']\n\njobs:\n  tests:\n    runs-on: ubuntu-latest\n    name: Tests\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: 8.4\n          extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo\n          coverage: none\n\n      - name: Install dependencies\n        run: composer install --prefer-dist --no-interaction\n\n      - name: Execute tests\n        run: vendor/bin/phpunit\n"
  },
  {
    "path": ".gitignore",
    "content": "/.phpunit.cache\n/node_modules\n/public/build\n/public/hot\n/public/storage\n/public/vendor\n/public/js\n/public/css\n/storage/*.key\n/vendor\n.env\n.env.backup\n.env.production\n.phpunit.result.cache\nHomestead.json\nHomestead.yaml\nauth.json\nnpm-debug.log\nyarn-error.log\n/.fleet\n/.idea\n/.vscode\n\n/caddy\nfrankenphp\nfrankenphp-worker.php\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM php:8.4-fpm-alpine\n\nRUN apk add --no-cache \\\n    bash \\\n    git \\\n    curl \\\n    vim \\\n    unzip \\\n    nginx \\\n    supervisor \\\n    dcron \\\n    su-exec \\\n    nodejs \\\n    npm \\\n    libzip-dev \\\n    libxml2-dev \\\n    icu-dev \\\n    libpng-dev \\\n    libjpeg-turbo-dev \\\n    freetype-dev \\\n    pango \\\n    linux-headers \\\n    python3 \\\n    py3-pip \\\n    py3-cffi \\\n    py3-brotli \\\n    && pip3 install --break-system-packages WeasyPrint\n\nRUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer\n\nRUN docker-php-ext-configure gd --with-freetype --with-jpeg \\\n    && docker-php-ext-install \\\n        pdo \\\n        pdo_mysql \\\n        sockets \\\n        pcntl \\\n        zip \\\n        exif \\\n        bcmath \\\n        intl \\\n        gd\n\nRUN apk add --no-cache --virtual .build-deps autoconf g++ make \\\n    && pecl install redis \\\n    && docker-php-ext-enable redis \\\n    && apk del .build-deps\n\nCOPY . /app\nWORKDIR /app\n\nENV COMPOSER_ALLOW_SUPERUSER=1\n\nRUN composer install --no-dev --prefer-dist --no-interaction\n\nRUN npm install\nRUN npm run build\n\nRUN mkdir -p /tmp/public/ \\\n    && cp -r /app/public/* /tmp/public/ \\\n    && chown -R www-data:www-data /app\n\nCOPY docker/nginx.conf /etc/nginx/http.d/default.conf\nCOPY docker/php-fpm.ini /usr/local/etc/php/conf.d/zzz-fpm-overrides.ini\nCOPY docker/www.conf /usr/local/etc/php-fpm.d/www.conf\n\n# Ensure the socket directory exists and is writable\nRUN mkdir -p /run && chown www-data:www-data /run\n\nRUN /usr/bin/crontab /app/docker/crontab\n\nENTRYPOINT [\"sh\", \"/app/docker/entrypoint.sh\"]\n"
  },
  {
    "path": "LICENSE.md",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\nCopyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\nEveryone is permitted to copy and distribute verbatim copies\nof this license document, but changing it is not allowed.\n\n                            Preamble\n\nThe GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\nThe licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\nWhen we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\nDevelopers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\nA secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\nThe GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\nAn older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\nThe precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n0. Definitions.\n\n\"This License\" refers to version 3 of the GNU Affero General Public License.\n\n\"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n\"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\nTo \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\nA \"covered work\" means either the unmodified Program or a work based\non the Program.\n\nTo \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\nTo \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\nAn interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n1. Source Code.\n\nThe \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\nA \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\nThe \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\nThe \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\nThe Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\nThe Corresponding Source for a work in source code form is that\nsame work.\n\n2. Basic Permissions.\n\nAll rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\nYou may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\nConveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\nNo covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\nWhen you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n4. Conveying Verbatim Copies.\n\nYou may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\nYou may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n5. Conveying Modified Source Versions.\n\nYou may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\nA compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n6. Conveying Non-Source Forms.\n\nYou may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\nA separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\nA \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n\"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\nIf you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\nThe requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\nCorresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n7. Additional Terms.\n\n\"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\nWhen you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\nNotwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\nAll other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\nIf you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\nAdditional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n8. Termination.\n\nYou may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\nHowever, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\nMoreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\nTermination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n9. Acceptance Not Required for Having Copies.\n\nYou are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n10. Automatic Licensing of Downstream Recipients.\n\nEach time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\nAn \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\nYou may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n11. Patents.\n\nA \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\nA contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\nEach contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\nIn the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\nIf you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\nIf, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\nA patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\nNothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n12. No Surrender of Others' Freedom.\n\nIf conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n13. Remote Network Interaction; Use with the GNU General Public License.\n\nNotwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\nNotwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n14. Revised Versions of this License.\n\nThe Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\nIf the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\nLater license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n15. Disclaimer of Warranty.\n\nTHERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n16. Limitation of Liability.\n\nIN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n17. Interpretation of Sections 15 and 16.\n\nIf the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\nYou should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <a href=\"https://govigilant.io/\" target=\"_blank\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://govigilant.io/img/githubheader-dark.svg\">\n      <img alt=\"Vigilant Logo\" src=\"https://govigilant.io/img/githubheader-light.svg\" width=\"400px\"/>\n    </picture>\n  </a>\n</p>\n\n<div align=\"center\">\n  <strong>\n  <h2>All-in-One Web Monitoring Tool</h2><br />\n  <a href=\"https://govigilant.io\">Vigilant</a>: An application designed to monitor all aspects of your website<br /><br />\n  </strong>\n  Monitor all aspects of your website, uptime, health, DNS, Lighthouse, broken links...<br/>\n  Get notified exactly where and when you want with customizable notifications. <br/>\n  Comes with sensible defaults to quickly get started but customizable to fit your needs.<br/>\n</div>\n\n<p align=\"center\">\n  <br />\n  <a href=\"https://govigilant.io/documentation/welcome\" rel=\"dofollow\"><strong>Explore the docs »</strong></a>\n  <br />\n\n  <br/>\n    <a href=\"https://app.govigilant.io\">Get started</a>\n    ·\n    <a href=\"https://discord.gg/MG3aV8uFt5\">Join the Discord</a>\n</p>\n\n![Vigilant Screenshot](https://govigilant.io/screenshot.png)\n\n\n## Getting Started with Vigilant\n\nQuickly get started using the hosted version of Vigilant at [app.govigilant.io](https://app.govigilant.io).\n\nPrefer to self-host?\nTake a look at the [documentation](https://govigilant.io/documentation/welcome) on how to get started!\n\n## Tech Stack\n\n- Laravel\n- Livewire\n- TailwindCSS\n- AlpineJS\n- Redis\n- MySQL\n\n## License\n\nVigilant is open source under the GNU Affero General Public License Version 3 (AGPLv3) or any later version.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Introduction\n\nVigilant is committed to ensuring the security and integrity of our users' data. This security policy outlines our procedures for handling security vulnerabilities and our disclosure policy.\n\n## Reporting Security Vulnerabilities\n\nIf you discover a security vulnerability in Vigilant, please report it to us privately via email to one of the maintainers:\n\n* @VincentBean ([email](mailto:security@govigilant.io))\n\nWhen reporting a security vulnerability, please provide as much detail as possible, including:\n\n* A clear description of the vulnerability\n* Steps to reproduce the vulnerability\n* Any relevant code or configuration files\n\n## Supported Versions\n\nThis project currently only supports the latest release. We recommend that users always use the latest version of Vigilant to ensure they have the latest security patches.\n\n## Disclosure Guidelines\n\nWe follow a private disclosure policy. If you discover a security vulnerability, please report it to us privately via email to one of the maintainers listed above. We will respond promptly to reports of vulnerabilities and work to resolve them as quickly as possible.\n\nWe will not publicly disclose security vulnerabilities until a patch or fix is available to prevent malicious actors from exploiting the vulnerability before a fix is released.\n\n## Security Vulnerability Response Process\n\nWe take security vulnerabilities seriously and will respond promptly to reports of vulnerabilities. Our response process includes:\n\n* Investigating the report and verifying the vulnerability.\n* Developing a patch or fix for the vulnerability.\n* Releasing the patch or fix as soon as possible.\n* Notifying users of the vulnerability and the patch or fix.\n\n## Template Attribution\n\nThis SECURITY.md file is based on the [GitHub Security Policy Template](https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository).\n\nThank you for helping to keep Vigilant secure!\n"
  },
  {
    "path": "app/Console/Kernel.php",
    "content": "<?php\n\nnamespace App\\Console;\n\nuse Illuminate\\Console\\Scheduling\\Schedule;\nuse Illuminate\\Foundation\\Console\\Kernel as ConsoleKernel;\nuse Vigilant\\Certificates\\Commands\\CheckCertificatesCommand;\nuse Vigilant\\Crawler\\Commands\\CrawlUrlsCommand;\nuse Vigilant\\Crawler\\Commands\\ProcessCrawlerStatesCommand;\nuse Vigilant\\Crawler\\Commands\\ScheduleCrawlersCommand;\nuse Vigilant\\Cve\\Commands\\ImportCvesCommand;\nuse Vigilant\\Dns\\Commands\\CheckAllDnsRecordsCommand;\nuse Vigilant\\Healthchecks\\Commands\\AggregateMetricsCommand;\nuse Vigilant\\Healthchecks\\Commands\\ScheduleHealthchecksCommand;\nuse Vigilant\\Lighthouse\\Commands\\AggregateLighthouseResultsCommand;\nuse Vigilant\\Lighthouse\\Commands\\ScheduleLighthouseCommand;\nuse Vigilant\\Notifications\\Commands\\CreateNotificationsCommand;\nuse Vigilant\\Uptime\\Commands\\AggregateResultsCommand;\nuse Vigilant\\Uptime\\Commands\\CheckUnavailableOutpostsCommand;\nuse Vigilant\\Uptime\\Commands\\ScheduleUptimeChecksCommand;\n\nclass Kernel extends ConsoleKernel\n{\n    protected function schedule(Schedule $schedule): void\n    {\n        // Uptime\n        $schedule->command(AggregateResultsCommand::class)->hourly();\n        $schedule->command(ScheduleUptimeChecksCommand::class)->everySecond();\n        $schedule->command(CheckUnavailableOutpostsCommand::class)->everyFifteenMinutes();\n\n        // Healthchecks\n        $schedule->command(ScheduleHealthchecksCommand::class)->everySecond();\n        $schedule->command(AggregateMetricsCommand::class)->hourly();\n\n        // Lighthouse\n        $schedule->command(ScheduleLighthouseCommand::class)->everySecond();\n        $schedule->command(AggregateLighthouseResultsCommand::class)->daily();\n\n        // Dns\n        $schedule->command(CheckAllDnsRecordsCommand::class)->hourly();\n\n        // Crawler\n        $schedule->command(ScheduleCrawlersCommand::class)->everyMinute();\n        $schedule->command(CrawlUrlsCommand::class)->everyMinute();\n        $schedule->command(ProcessCrawlerStatesCommand::class)->everyMinute();\n\n        // Certificates\n        $schedule->command(CheckCertificatesCommand::class)->everyMinute();\n\n        // CVE\n        $schedule->command(ImportCvesCommand::class, ['now - 1 hour'])->everyThirtyMinutes();\n\n        // Notifications\n        $schedule->command(CreateNotificationsCommand::class)->daily();\n\n        $schedule->command('model:prune', [\n            '--model' => array_keys(config('core.data_retention')),\n        ])->hourly();\n    }\n\n    protected function commands(): void\n    {\n        $this->load(__DIR__.'/Commands');\n\n        require base_path('routes/console.php');\n    }\n}\n"
  },
  {
    "path": "app/Exceptions/Handler.php",
    "content": "<?php\n\nnamespace App\\Exceptions;\n\nuse Illuminate\\Foundation\\Exceptions\\Handler as ExceptionHandler;\nuse Throwable;\n\nclass Handler extends ExceptionHandler\n{\n    /**\n     * The list of the inputs that are never flashed to the session on validation exceptions.\n     *\n     * @var array<int, string>\n     */\n    protected $dontFlash = [\n        'current_password',\n        'password',\n        'password_confirmation',\n    ];\n\n    /**\n     * Register the exception handling callbacks for the application.\n     */\n    public function register(): void\n    {\n        $this->reportable(function (Throwable $e) {\n            //\n        });\n    }\n}\n"
  },
  {
    "path": "app/Http/Controllers/Controller.php",
    "content": "<?php\n\nnamespace App\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Auth\\Access\\AuthorizesRequests;\nuse Illuminate\\Foundation\\Validation\\ValidatesRequests;\nuse Illuminate\\Routing\\Controller as BaseController;\n\nclass Controller extends BaseController\n{\n    use AuthorizesRequests, ValidatesRequests;\n}\n"
  },
  {
    "path": "app/Http/Kernel.php",
    "content": "<?php\n\nnamespace App\\Http;\n\nuse Illuminate\\Foundation\\Http\\Kernel as HttpKernel;\nuse Illuminate\\Support\\Arr;\nuse Vigilant\\Core\\Http\\Middleware\\TeamMiddleware;\nuse Vigilant\\OnBoarding\\Http\\Middleware\\RedirectToOnboard;\nuse Vigilant\\Users\\Http\\Middleware\\EnsureEmailIsVerified;\n\nclass Kernel extends HttpKernel\n{\n    /**\n     * The application's global HTTP middleware stack.\n     * These middleware are run during every request to your application.\n     *\n     * @var array<int, class-string|string>\n     */\n    protected $middleware = [\n        // \\App\\Http\\Middleware\\TrustHosts::class,\n        \\App\\Http\\Middleware\\TrustProxies::class,\n        \\Illuminate\\Http\\Middleware\\HandleCors::class,\n        \\App\\Http\\Middleware\\PreventRequestsDuringMaintenance::class,\n        \\Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize::class,\n        \\App\\Http\\Middleware\\TrimStrings::class,\n        \\Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull::class,\n    ];\n\n    /**\n     * The application's route middleware groups.\n     *\n     * @var array<string, array<int, class-string|string>>\n     */\n    protected $middlewareGroups = [\n        'web' => [\n            \\App\\Http\\Middleware\\EncryptCookies::class,\n            \\Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse::class,\n            \\Illuminate\\Session\\Middleware\\StartSession::class,\n            \\Illuminate\\View\\Middleware\\ShareErrorsFromSession::class,\n            \\App\\Http\\Middleware\\VerifyCsrfToken::class,\n            \\Illuminate\\Routing\\Middleware\\SubstituteBindings::class,\n            TeamMiddleware::class,\n            RedirectToOnboard::class,\n            EnsureEmailIsVerified::class,\n        ],\n\n        'api' => [\n            // \\Laravel\\Sanctum\\Http\\Middleware\\EnsureFrontendRequestsAreStateful::class,\n            \\Illuminate\\Routing\\Middleware\\ThrottleRequests::class.':api',\n            \\Illuminate\\Routing\\Middleware\\SubstituteBindings::class,\n        ],\n    ];\n\n    /**\n     * The application's middleware aliases.\n     * Aliases may be used instead of class names to conveniently assign middleware to routes and groups.\n     *\n     * @var array<string, class-string|string>\n     */\n    protected $middlewareAliases = [\n        'auth' => \\App\\Http\\Middleware\\Authenticate::class,\n        'auth.basic' => \\Illuminate\\Auth\\Middleware\\AuthenticateWithBasicAuth::class,\n        'auth.session' => \\Illuminate\\Session\\Middleware\\AuthenticateSession::class,\n        'cache.headers' => \\Illuminate\\Http\\Middleware\\SetCacheHeaders::class,\n        'can' => \\Illuminate\\Auth\\Middleware\\Authorize::class,\n        'guest' => \\App\\Http\\Middleware\\RedirectIfAuthenticated::class,\n        'password.confirm' => \\Illuminate\\Auth\\Middleware\\RequirePassword::class,\n        'precognitive' => \\Illuminate\\Foundation\\Http\\Middleware\\HandlePrecognitiveRequests::class,\n        'signed' => \\App\\Http\\Middleware\\ValidateSignature::class,\n        'throttle' => \\Illuminate\\Routing\\Middleware\\ThrottleRequests::class,\n        'verified' => EnsureEmailIsVerified::class,\n    ];\n\n    public function addMiddlewareToGroup(string $group, string|array $middleware): void\n    {\n        $this->middlewareGroups[$group] = array_merge($this->middlewareGroups[$group], Arr::wrap($middleware));\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/Authenticate.php",
    "content": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Illuminate\\Auth\\Middleware\\Authenticate as Middleware;\nuse Illuminate\\Http\\Request;\n\nclass Authenticate extends Middleware\n{\n    /**\n     * Get the path the user should be redirected to when they are not authenticated.\n     */\n    protected function redirectTo(Request $request): ?string\n    {\n        return $request->expectsJson() ? null : route('login');\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/EncryptCookies.php",
    "content": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Illuminate\\Cookie\\Middleware\\EncryptCookies as Middleware;\n\nclass EncryptCookies extends Middleware\n{\n    /**\n     * The names of the cookies that should not be encrypted.\n     *\n     * @var array<int, string>\n     */\n    protected $except = [\n        //\n    ];\n}\n"
  },
  {
    "path": "app/Http/Middleware/PreventRequestsDuringMaintenance.php",
    "content": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance as Middleware;\n\nclass PreventRequestsDuringMaintenance extends Middleware\n{\n    /**\n     * The URIs that should be reachable while maintenance mode is enabled.\n     *\n     * @var array<int, string>\n     */\n    protected $except = [\n        //\n    ];\n}\n"
  },
  {
    "path": "app/Http/Middleware/RedirectIfAuthenticated.php",
    "content": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse App\\Providers\\RouteServiceProvider;\nuse Closure;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Auth;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass RedirectIfAuthenticated\n{\n    /**\n     * Handle an incoming request.\n     *\n     * @param  \\Closure(\\Illuminate\\Http\\Request): (\\Symfony\\Component\\HttpFoundation\\Response)  $next\n     */\n    public function handle(Request $request, Closure $next, string ...$guards): Response\n    {\n        $guards = empty($guards) ? [null] : $guards;\n\n        foreach ($guards as $guard) {\n            if (Auth::guard($guard)->check()) {\n                return redirect(RouteServiceProvider::HOME);\n            }\n        }\n\n        return $next($request);\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/TrimStrings.php",
    "content": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Illuminate\\Foundation\\Http\\Middleware\\TrimStrings as Middleware;\n\nclass TrimStrings extends Middleware\n{\n    /**\n     * The names of the attributes that should not be trimmed.\n     *\n     * @var array<int, string>\n     */\n    protected $except = [\n        'current_password',\n        'password',\n        'password_confirmation',\n    ];\n}\n"
  },
  {
    "path": "app/Http/Middleware/TrustHosts.php",
    "content": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Illuminate\\Http\\Middleware\\TrustHosts as Middleware;\n\nclass TrustHosts extends Middleware\n{\n    /**\n     * Get the host patterns that should be trusted.\n     *\n     * @return array<int, string|null>\n     */\n    public function hosts(): array\n    {\n        return [\n            $this->allSubdomainsOfApplicationUrl(),\n        ];\n    }\n}\n"
  },
  {
    "path": "app/Http/Middleware/TrustProxies.php",
    "content": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Illuminate\\Http\\Middleware\\TrustProxies as Middleware;\nuse Illuminate\\Http\\Request;\n\nclass TrustProxies extends Middleware\n{\n    /**\n     * The trusted proxies for this application.\n     *\n     * @var array<int, string>|string|null\n     */\n    protected $proxies = '*';\n\n    /**\n     * The headers that should be used to detect proxies.\n     *\n     * @var int\n     */\n    protected $headers =\n        Request::HEADER_X_FORWARDED_FOR |\n        Request::HEADER_X_FORWARDED_HOST |\n        Request::HEADER_X_FORWARDED_PORT |\n        Request::HEADER_X_FORWARDED_PROTO |\n        Request::HEADER_X_FORWARDED_AWS_ELB;\n}\n"
  },
  {
    "path": "app/Http/Middleware/ValidateSignature.php",
    "content": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Illuminate\\Routing\\Middleware\\ValidateSignature as Middleware;\n\nclass ValidateSignature extends Middleware\n{\n    /**\n     * The names of the query string parameters that should be ignored.\n     *\n     * @var array<int, string>\n     */\n    protected $except = [\n        // 'fbclid',\n        // 'utm_campaign',\n        // 'utm_content',\n        // 'utm_medium',\n        // 'utm_source',\n        // 'utm_term',\n    ];\n}\n"
  },
  {
    "path": "app/Http/Middleware/VerifyCsrfToken.php",
    "content": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken as Middleware;\n\nclass VerifyCsrfToken extends Middleware\n{\n    /**\n     * The URIs that should be excluded from CSRF verification.\n     *\n     * @var array<int, string>\n     */\n    protected $except = [\n        'paddle/*',\n    ];\n}\n"
  },
  {
    "path": "app/Providers/AppServiceProvider.php",
    "content": "<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Support\\Facades\\URL;\nuse Illuminate\\Support\\ServiceProvider;\nuse Vigilant\\Core\\Facades\\Navigation;\n\nclass AppServiceProvider extends ServiceProvider\n{\n    /**\n     * Register any application services.\n     */\n    public function register(): void\n    {\n        //\n    }\n\n    /**\n     * Bootstrap any application services.\n     */\n    public function boot(): void\n    {\n        Navigation::path(resource_path('navigation.php'));\n\n        $url = config('app.url', '');\n\n        if (str_starts_with($url, 'https')) {\n            URL::forceRootUrl($url);\n            URL::forceScheme('https');\n        }\n    }\n}\n"
  },
  {
    "path": "app/Providers/AuthServiceProvider.php",
    "content": "<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Foundation\\Support\\Providers\\AuthServiceProvider as ServiceProvider;\n\nclass AuthServiceProvider extends ServiceProvider\n{\n    /**\n     * The model to policy mappings for the application.\n     *\n     * @var array<class-string, class-string>\n     */\n    protected $policies = [\n\n    ];\n\n    /**\n     * Register any authentication / authorization services.\n     */\n    public function boot(): void\n    {\n        //\n    }\n}\n"
  },
  {
    "path": "app/Providers/BroadcastServiceProvider.php",
    "content": "<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Support\\Facades\\Broadcast;\nuse Illuminate\\Support\\ServiceProvider;\n\nclass BroadcastServiceProvider extends ServiceProvider\n{\n    /**\n     * Bootstrap any application services.\n     */\n    public function boot(): void\n    {\n        Broadcast::routes();\n\n        require base_path('routes/channels.php');\n    }\n}\n"
  },
  {
    "path": "app/Providers/EventServiceProvider.php",
    "content": "<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Auth\\Events\\Registered;\nuse Illuminate\\Auth\\Listeners\\SendEmailVerificationNotification;\nuse Illuminate\\Foundation\\Support\\Providers\\EventServiceProvider as ServiceProvider;\n\nclass EventServiceProvider extends ServiceProvider\n{\n    /**\n     * The event to listener mappings for the application.\n     *\n     * @var array<class-string, array<int, class-string>>\n     */\n    protected $listen = [\n        Registered::class => [\n            SendEmailVerificationNotification::class,\n        ],\n    ];\n\n    /**\n     * Register any events for your application.\n     */\n    public function boot(): void\n    {\n        //\n    }\n\n    /**\n     * Determine if events and listeners should be automatically discovered.\n     */\n    public function shouldDiscoverEvents(): bool\n    {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/Providers/FortifyServiceProvider.php",
    "content": "<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\RateLimiter;\nuse Illuminate\\Support\\ServiceProvider;\nuse Illuminate\\Support\\Str;\nuse Laravel\\Fortify\\Fortify;\nuse Vigilant\\Users\\Actions\\Fortify\\CreateNewUser;\nuse Vigilant\\Users\\Actions\\Fortify\\ResetUserPassword;\nuse Vigilant\\Users\\Actions\\Fortify\\UpdateUserPassword;\nuse Vigilant\\Users\\Actions\\Fortify\\UpdateUserProfileInformation;\n\nclass FortifyServiceProvider extends ServiceProvider\n{\n    /**\n     * Register any application services.\n     */\n    public function register(): void\n    {\n        //\n    }\n\n    /**\n     * Bootstrap any application services.\n     */\n    public function boot(): void\n    {\n        Fortify::createUsersUsing(CreateNewUser::class);\n        Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);\n        Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);\n        Fortify::resetUserPasswordsUsing(ResetUserPassword::class);\n\n        RateLimiter::for('login', function (Request $request) {\n            $throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());\n\n            return Limit::perMinute(5)->by($throttleKey);\n        });\n\n        RateLimiter::for('two-factor', function (Request $request) {\n            return Limit::perMinute(5)->by($request->session()->get('login.id'));\n        });\n    }\n}\n"
  },
  {
    "path": "app/Providers/HorizonServiceProvider.php",
    "content": "<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Support\\Facades\\Gate;\nuse Laravel\\Horizon\\Horizon;\nuse Laravel\\Horizon\\HorizonApplicationServiceProvider;\n\nclass HorizonServiceProvider extends HorizonApplicationServiceProvider\n{\n    /**\n     * Bootstrap any application services.\n     */\n    public function boot(): void\n    {\n        parent::boot();\n\n        // Horizon::routeSmsNotificationsTo('15556667777');\n        // Horizon::routeMailNotificationsTo('example@example.com');\n        // Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');\n    }\n\n    /**\n     * Register the Horizon gate.\n     * This gate determines who can access Horizon in non-local environments.\n     */\n    protected function gate(): void\n    {\n        if (ce()) {\n            Gate::define('viewHorizon', function ($user) {\n                return true;\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "app/Providers/JetstreamServiceProvider.php",
    "content": "<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Support\\ServiceProvider;\nuse Laravel\\Jetstream\\Jetstream;\nuse Vigilant\\Users\\Actions\\Jetstream\\AddTeamMember;\nuse Vigilant\\Users\\Actions\\Jetstream\\CreateTeam;\nuse Vigilant\\Users\\Actions\\Jetstream\\DeleteTeam;\nuse Vigilant\\Users\\Actions\\Jetstream\\DeleteUser;\nuse Vigilant\\Users\\Actions\\Jetstream\\InviteTeamMember;\nuse Vigilant\\Users\\Actions\\Jetstream\\RemoveTeamMember;\nuse Vigilant\\Users\\Actions\\Jetstream\\UpdateTeamName;\nuse Vigilant\\Users\\Models\\Membership;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Models\\TeamInvitation;\nuse Vigilant\\Users\\Models\\User;\n\nclass JetstreamServiceProvider extends ServiceProvider\n{\n    /**\n     * Register any application services.\n     */\n    public function register(): void\n    {\n        //\n    }\n\n    /**\n     * Bootstrap any application services.\n     */\n    public function boot(): void\n    {\n        $this->configurePermissions();\n\n        Jetstream::createTeamsUsing(CreateTeam::class);\n        Jetstream::updateTeamNamesUsing(UpdateTeamName::class);\n        Jetstream::addTeamMembersUsing(AddTeamMember::class);\n        Jetstream::inviteTeamMembersUsing(InviteTeamMember::class);\n        Jetstream::removeTeamMembersUsing(RemoveTeamMember::class);\n        Jetstream::deleteTeamsUsing(DeleteTeam::class);\n        Jetstream::deleteUsersUsing(DeleteUser::class);\n\n        Jetstream::useUserModel(User::class);\n        Jetstream::useTeamModel(Team::class);\n        Jetstream::useTeamInvitationModel(TeamInvitation::class);\n        Jetstream::useMembershipModel(Membership::class);\n    }\n\n    /**\n     * Configure the roles and permissions that are available within the application.\n     */\n    protected function configurePermissions(): void\n    {\n        Jetstream::defaultApiTokenPermissions(['read']);\n\n        Jetstream::role('admin', 'Administrator', [\n            'create',\n            'read',\n            'update',\n            'delete',\n        ])->description('Administrator users can perform any action.');\n    }\n}\n"
  },
  {
    "path": "app/Providers/RouteServiceProvider.php",
    "content": "<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Foundation\\Support\\Providers\\RouteServiceProvider as ServiceProvider;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\RateLimiter;\nuse Illuminate\\Support\\Facades\\Route;\n\nclass RouteServiceProvider extends ServiceProvider\n{\n    /**\n     * The path to your application's \"home\" route.\n     *\n     * Typically, users are redirected here after authentication.\n     *\n     * @var string\n     */\n    public const HOME = '/';\n\n    /**\n     * Define your route model bindings, pattern filters, and other route configuration.\n     */\n    public function boot(): void\n    {\n        RateLimiter::for('api', function (Request $request) {\n            return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());\n        });\n\n        $this->routes(function () {\n            Route::middleware('api')\n                ->prefix('api')\n                ->group(base_path('routes/api.php'));\n\n            Route::middleware('web')\n                ->group(base_path('routes/web.php'));\n        });\n    }\n}\n"
  },
  {
    "path": "app/View/Components/AppLayout.php",
    "content": "<?php\n\nnamespace App\\View\\Components;\n\nuse Illuminate\\View\\Component;\nuse Illuminate\\View\\View;\n\nclass AppLayout extends Component\n{\n    /**\n     * Get the view / contents that represents the component.\n     */\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'layouts.app';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "app/View/Components/GuestLayout.php",
    "content": "<?php\n\nnamespace App\\View\\Components;\n\nuse Illuminate\\View\\Component;\nuse Illuminate\\View\\View;\n\nclass GuestLayout extends Component\n{\n    /**\n     * Get the view / contents that represents the component.\n     */\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'layouts.guest';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "artisan",
    "content": "#!/usr/bin/env php\n<?php\n\ndefine('LARAVEL_START', microtime(true));\n\n/*\n|--------------------------------------------------------------------------\n| Register The Auto Loader\n|--------------------------------------------------------------------------\n|\n| Composer provides a convenient, automatically generated class loader\n| for our application. We just need to utilize it! We'll require it\n| into the script here so that we do not have to worry about the\n| loading of any of our classes manually. It's great to relax.\n|\n*/\n\nrequire __DIR__.'/vendor/autoload.php';\n\n$app = require_once __DIR__.'/bootstrap/app.php';\n\n/*\n|--------------------------------------------------------------------------\n| Run The Artisan Application\n|--------------------------------------------------------------------------\n|\n| When we run the console application, the current CLI command will be\n| executed in this console and the response sent back to a terminal\n| or another output device for the developers. Here goes nothing!\n|\n*/\n\n$kernel = $app->make(Illuminate\\Contracts\\Console\\Kernel::class);\n\n$status = $kernel->handle(\n    $input = new Symfony\\Component\\Console\\Input\\ArgvInput,\n    new Symfony\\Component\\Console\\Output\\ConsoleOutput\n);\n\n/*\n|--------------------------------------------------------------------------\n| Shutdown The Application\n|--------------------------------------------------------------------------\n|\n| Once Artisan has finished running, we will fire off the shutdown events\n| so that any final work may be done by the application before we shut\n| down the process. This is the last thing to happen to the request.\n|\n*/\n\n$kernel->terminate($input, $status);\n\nexit($status);\n"
  },
  {
    "path": "bootstrap/app.php",
    "content": "<?php\n\n/*\n|--------------------------------------------------------------------------\n| Create The Application\n|--------------------------------------------------------------------------\n|\n| The first thing we will do is create a new Laravel application instance\n| which serves as the \"glue\" for all the components of Laravel, and is\n| the IoC container for the system binding all of the various parts.\n|\n*/\n\n$app = new Illuminate\\Foundation\\Application(\n    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)\n);\n\n/*\n|--------------------------------------------------------------------------\n| Bind Important Interfaces\n|--------------------------------------------------------------------------\n|\n| Next, we need to bind some important interfaces into the container so\n| we will be able to resolve them when needed. The kernels serve the\n| incoming requests to this application from both the web and CLI.\n|\n*/\n\n$app->singleton(\n    Illuminate\\Contracts\\Http\\Kernel::class,\n    App\\Http\\Kernel::class\n);\n\n$app->singleton(\n    Illuminate\\Contracts\\Console\\Kernel::class,\n    App\\Console\\Kernel::class\n);\n\n$app->singleton(\n    Illuminate\\Contracts\\Debug\\ExceptionHandler::class,\n    App\\Exceptions\\Handler::class\n);\n\n/*\n|--------------------------------------------------------------------------\n| Return The Application\n|--------------------------------------------------------------------------\n|\n| This script returns the application instance. The instance is given to\n| the calling script so we can separate the building of the instances\n| from the actual running of the application and sending responses.\n|\n*/\n\nreturn $app;\n"
  },
  {
    "path": "bootstrap/cache/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"laravel/laravel\",\n    \"type\": \"project\",\n    \"description\": \"Vigilant: All-in-one website monitoring\",\n    \"keywords\": [\n        \"monitoring\",\n        \"laravel\",\n        \"vigilant\"\n    ],\n    \"license\": \"AGPL-3.0\",\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"ext-dom\": \"*\",\n        \"blade-ui-kit/blade-heroicons\": \"^2.3\",\n        \"bluelibraries/dns\": \"@dev\",\n        \"codeat3/blade-carbon-icons\": \"^2.35\",\n        \"codeat3/blade-line-awesome-icons\": \"*\",\n        \"codeat3/blade-phosphor-icons\": \"^2.2\",\n        \"codeat3/blade-simple-icons\": \"^7.16\",\n        \"codeat3/blade-teeny-icons\": \"^1.9\",\n        \"govigilant/laravel-healthchecks\": \"^1.0\",\n        \"guzzlehttp/guzzle\": \"^7.2\",\n        \"laravel/framework\": \"^12.0\",\n        \"laravel/horizon\": \"^5.23\",\n        \"laravel/jetstream\": \"^5.0\",\n        \"laravel/sanctum\": \"^4.0\",\n        \"laravel/socialite\": \"^5.18\",\n        \"laravel/tinker\": \"^2.8\",\n        \"league/iso3166\": \"^4.3\",\n        \"livewire/livewire\": \"^3.0\",\n        \"mallardduck/blade-boxicons\": \"^2.4\",\n        \"ramonrietdijk/livewire-tables\": \"^6.0\",\n        \"vigilant/certificates\": \"@dev\",\n        \"vigilant/core\": \"@dev\",\n        \"vigilant/crawler\": \"@dev\",\n        \"vigilant/cve\": \"@dev\",\n        \"vigilant/dns\": \"@dev\",\n        \"vigilant/frontend\": \"@dev\",\n        \"vigilant/healthchecks\": \"@dev\",\n        \"vigilant/lighthouse\": \"@dev\",\n        \"vigilant/notifications\": \"@dev\",\n        \"vigilant/onboarding\": \"@dev\",\n        \"vigilant/settings\": \"@dev\",\n        \"vigilant/sites\": \"@dev\",\n        \"vigilant/uptime\": \"@dev\",\n        \"vigilant/users\": \"@dev\"\n    },\n    \"require-dev\": {\n        \"fakerphp/faker\": \"^1.9.1\",\n        \"laravel/dusk\": \"^8.1\",\n        \"laravel/pint\": \"^1.0\",\n        \"laravel/sail\": \"^1.18\",\n        \"mockery/mockery\": \"^1.4.4\",\n        \"nunomaduro/collision\": \"^8.1\",\n        \"phpunit/phpunit\": \"^11.0\",\n        \"spatie/laravel-ignition\": \"^2.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"App\\\\\": \"app/\",\n            \"Database\\\\Factories\\\\\": \"database/factories/\",\n            \"Database\\\\Seeders\\\\\": \"database/seeders/\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Tests\\\\\": \"tests/\"\n        }\n    },\n    \"scripts\": {\n        \"post-autoload-dump\": [\n            \"Illuminate\\\\Foundation\\\\ComposerScripts::postAutoloadDump\",\n            \"@php artisan package:discover --ansi\"\n        ],\n        \"post-update-cmd\": [\n            \"@php artisan vendor:publish --tag=laravel-assets --ansi --force\"\n        ],\n        \"post-install-cmd\": [\n            \"@php artisan vendor:publish --tag=laravel-assets --ansi --force\"\n        ],\n        \"post-root-package-install\": [\n            \"@php -r \\\"file_exists('.env') || copy('.env.example', '.env');\\\"\"\n        ],\n        \"post-create-project-cmd\": [\n            \"@php artisan key:generate --ansi\"\n        ]\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"dont-discover\": []\n        }\n    },\n    \"config\": {\n        \"optimize-autoloader\": true,\n        \"preferred-install\": \"dist\",\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"pestphp/pest-plugin\": true,\n            \"php-http/discovery\": true\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"./packages/*\"\n        },\n        {\n            \"type\": \"vcs\",\n            \"url\": \"git@github.com:VincentBean/dns.git\"\n        }\n    ]\n}\n"
  },
  {
    "path": "config/app.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Facade;\nuse Illuminate\\Support\\ServiceProvider;\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Application Name\n    |--------------------------------------------------------------------------\n    |\n    | This value is the name of your application. This value is used when the\n    | framework needs to place the application's name in a notification or\n    | any other location as required by the application or its packages.\n    |\n    */\n\n    'name' => env('APP_NAME', 'Vigilant'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Application Environment\n    |--------------------------------------------------------------------------\n    |\n    | This value determines the \"environment\" your application is currently\n    | running in. This may determine how you prefer to configure various\n    | services the application utilizes. Set this in your \".env\" file.\n    |\n    */\n\n    'env' => env('APP_ENV', 'production'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Application Debug Mode\n    |--------------------------------------------------------------------------\n    |\n    | When your application is in debug mode, detailed error messages with\n    | stack traces will be shown on every error that occurs within your\n    | application. If disabled, a simple generic error page is shown.\n    |\n    */\n\n    'debug' => (bool) env('APP_DEBUG', false),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Application URL\n    |--------------------------------------------------------------------------\n    |\n    | This URL is used by the console to properly generate URLs when using\n    | the Artisan command line tool. You should set this to the root of\n    | your application so that it is used when running Artisan tasks.\n    |\n    */\n\n    'url' => env('APP_URL', 'http://localhost'),\n\n    'asset_url' => env('ASSET_URL'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Application Timezone\n    |--------------------------------------------------------------------------\n    |\n    | Here you may specify the default timezone for your application, which\n    | will be used by the PHP date and date-time functions. We have gone\n    | ahead and set this to a sensible default for you out of the box.\n    |\n    */\n\n    'timezone' => 'UTC',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Application Locale Configuration\n    |--------------------------------------------------------------------------\n    |\n    | The application locale determines the default locale that will be used\n    | by the translation service provider. You are free to set this value\n    | to any of the locales which will be supported by the application.\n    |\n    */\n\n    'locale' => 'en',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Application Fallback Locale\n    |--------------------------------------------------------------------------\n    |\n    | The fallback locale determines the locale to use when the current one\n    | is not available. You may change the value to correspond to any of\n    | the language folders that are provided through your application.\n    |\n    */\n\n    'fallback_locale' => 'en',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Faker Locale\n    |--------------------------------------------------------------------------\n    |\n    | This locale will be used by the Faker PHP library when generating fake\n    | data for your database seeds. For example, this will be used to get\n    | localized telephone numbers, street address information and more.\n    |\n    */\n\n    'faker_locale' => 'en_US',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Encryption Key\n    |--------------------------------------------------------------------------\n    |\n    | This key is used by the Illuminate encrypter service and should be set\n    | to a random, 32 character string, otherwise these encrypted strings\n    | will not be safe. Please do this before deploying an application!\n    |\n    */\n\n    'key' => env('APP_KEY'),\n\n    'cipher' => 'AES-256-CBC',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Maintenance Mode Driver\n    |--------------------------------------------------------------------------\n    |\n    | These configuration options determine the driver used to determine and\n    | manage Laravel's \"maintenance mode\" status. The \"cache\" driver will\n    | allow maintenance mode to be controlled across multiple machines.\n    |\n    | Supported drivers: \"file\", \"cache\"\n    |\n    */\n\n    'maintenance' => [\n        'driver' => 'file',\n        // 'store' => 'redis',\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Autoloaded Service Providers\n    |--------------------------------------------------------------------------\n    |\n    | The service providers listed here will be automatically loaded on the\n    | request to your application. Feel free to add your own services to\n    | this array to grant expanded functionality to your applications.\n    |\n    */\n\n    'providers' => ServiceProvider::defaultProviders()->merge([\n        /*\n         * Package Service Providers...\n         */\n\n        /*\n         * Application Service Providers...\n         */\n        App\\Providers\\AppServiceProvider::class,\n        App\\Providers\\AuthServiceProvider::class,\n        // App\\Providers\\BroadcastServiceProvider::class,\n        App\\Providers\\EventServiceProvider::class,\n        App\\Providers\\HorizonServiceProvider::class,\n        App\\Providers\\RouteServiceProvider::class,\n        App\\Providers\\FortifyServiceProvider::class,\n        App\\Providers\\JetstreamServiceProvider::class,\n    ])->toArray(),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Class Aliases\n    |--------------------------------------------------------------------------\n    |\n    | This array of class aliases will be registered when this application\n    | is started. However, feel free to register as many as you wish as\n    | the aliases are \"lazy\" loaded so they don't hinder performance.\n    |\n    */\n\n    'aliases' => Facade::defaultAliases()->merge([\n        // 'Example' => App\\Facades\\Example::class,\n    ])->toArray(),\n\n];\n"
  },
  {
    "path": "config/auth.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Authentication Defaults\n    |--------------------------------------------------------------------------\n    |\n    | This option controls the default authentication \"guard\" and password\n    | reset options for your application. You may change these defaults\n    | as required, but they're a perfect start for most applications.\n    |\n    */\n\n    'defaults' => [\n        'guard' => 'web',\n        'passwords' => 'users',\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Authentication Guards\n    |--------------------------------------------------------------------------\n    |\n    | Next, you may define every authentication guard for your application.\n    | Of course, a great default configuration has been defined for you\n    | here which uses session storage and the Eloquent user provider.\n    |\n    | All authentication drivers have a user provider. This defines how the\n    | users are actually retrieved out of your database or other storage\n    | mechanisms used by this application to persist your user's data.\n    |\n    | Supported: \"session\"\n    |\n    */\n\n    'guards' => [\n        'web' => [\n            'driver' => 'session',\n            'provider' => 'users',\n        ],\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | User Providers\n    |--------------------------------------------------------------------------\n    |\n    | All authentication drivers have a user provider. This defines how the\n    | users are actually retrieved out of your database or other storage\n    | mechanisms used by this application to persist your user's data.\n    |\n    | If you have multiple user tables or models you may configure multiple\n    | sources which represent each model / table. These sources may then\n    | be assigned to any extra authentication guards you have defined.\n    |\n    | Supported: \"database\", \"eloquent\"\n    |\n    */\n\n    'providers' => [\n        'users' => [\n            'driver' => 'eloquent',\n            'model' => \\Vigilant\\Users\\Models\\User::class,\n        ],\n\n        // 'users' => [\n        //     'driver' => 'database',\n        //     'table' => 'users',\n        // ],\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Resetting Passwords\n    |--------------------------------------------------------------------------\n    |\n    | You may specify multiple password reset configurations if you have more\n    | than one user table or model in the application and you want to have\n    | separate password reset settings based on the specific user types.\n    |\n    | The expiry time is the number of minutes that each reset token will be\n    | considered valid. This security feature keeps tokens short-lived so\n    | they have less time to be guessed. You may change this as needed.\n    |\n    | The throttle setting is the number of seconds a user must wait before\n    | generating more password reset tokens. This prevents the user from\n    | quickly generating a very large amount of password reset tokens.\n    |\n    */\n\n    'passwords' => [\n        'users' => [\n            'provider' => 'users',\n            'table' => 'password_reset_tokens',\n            'expire' => 60,\n            'throttle' => 60,\n        ],\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Password Confirmation Timeout\n    |--------------------------------------------------------------------------\n    |\n    | Here you may define the amount of seconds before a password confirmation\n    | times out and the user is prompted to re-enter their password via the\n    | confirmation screen. By default, the timeout lasts for three hours.\n    |\n    */\n\n    'password_timeout' => 10800,\n\n];\n"
  },
  {
    "path": "config/broadcasting.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Default Broadcaster\n    |--------------------------------------------------------------------------\n    |\n    | This option controls the default broadcaster that will be used by the\n    | framework when an event needs to be broadcast. You may set this to\n    | any of the connections defined in the \"connections\" array below.\n    |\n    | Supported: \"pusher\", \"ably\", \"redis\", \"log\", \"null\"\n    |\n    */\n\n    'default' => env('BROADCAST_DRIVER', 'null'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Broadcast Connections\n    |--------------------------------------------------------------------------\n    |\n    | Here you may define all of the broadcast connections that will be used\n    | to broadcast events to other systems or over websockets. Samples of\n    | each available type of connection are provided inside this array.\n    |\n    */\n\n    'connections' => [\n\n        'pusher' => [\n            'driver' => 'pusher',\n            'key' => env('PUSHER_APP_KEY'),\n            'secret' => env('PUSHER_APP_SECRET'),\n            'app_id' => env('PUSHER_APP_ID'),\n            'options' => [\n                'cluster' => env('PUSHER_APP_CLUSTER'),\n                'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',\n                'port' => env('PUSHER_PORT', 443),\n                'scheme' => env('PUSHER_SCHEME', 'https'),\n                'encrypted' => true,\n                'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',\n            ],\n            'client_options' => [\n                // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html\n            ],\n        ],\n\n        'ably' => [\n            'driver' => 'ably',\n            'key' => env('ABLY_KEY'),\n        ],\n\n        'redis' => [\n            'driver' => 'redis',\n            'connection' => 'default',\n        ],\n\n        'log' => [\n            'driver' => 'log',\n        ],\n\n        'null' => [\n            'driver' => 'null',\n        ],\n\n    ],\n\n];\n"
  },
  {
    "path": "config/cache.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Str;\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Default Cache Store\n    |--------------------------------------------------------------------------\n    |\n    | This option controls the default cache connection that gets used while\n    | using this caching library. This connection is used when another is\n    | not explicitly specified when executing a given caching function.\n    |\n    */\n\n    'default' => env('CACHE_DRIVER', 'redis'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Cache Stores\n    |--------------------------------------------------------------------------\n    |\n    | Here you may define all of the cache \"stores\" for your application as\n    | well as their drivers. You may even define multiple stores for the\n    | same cache driver to group types of items stored in your caches.\n    |\n    | Supported drivers: \"apc\", \"array\", \"database\", \"file\",\n    |         \"memcached\", \"redis\", \"dynamodb\", \"octane\", \"null\"\n    |\n    */\n\n    'stores' => [\n\n        'apc' => [\n            'driver' => 'apc',\n        ],\n\n        'array' => [\n            'driver' => 'array',\n            'serialize' => false,\n        ],\n\n        'database' => [\n            'driver' => 'database',\n            'table' => 'cache',\n            'connection' => null,\n            'lock_connection' => null,\n        ],\n\n        'file' => [\n            'driver' => 'file',\n            'path' => storage_path('framework/cache/data'),\n            'lock_path' => storage_path('framework/cache/data'),\n        ],\n\n        'memcached' => [\n            'driver' => 'memcached',\n            'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),\n            'sasl' => [\n                env('MEMCACHED_USERNAME'),\n                env('MEMCACHED_PASSWORD'),\n            ],\n            'options' => [\n                // Memcached::OPT_CONNECT_TIMEOUT => 2000,\n            ],\n            'servers' => [\n                [\n                    'host' => env('MEMCACHED_HOST', '127.0.0.1'),\n                    'port' => env('MEMCACHED_PORT', 11211),\n                    'weight' => 100,\n                ],\n            ],\n        ],\n\n        'redis' => [\n            'driver' => 'redis',\n            'connection' => 'cache',\n            'lock_connection' => 'default',\n        ],\n\n        'dynamodb' => [\n            'driver' => 'dynamodb',\n            'key' => env('AWS_ACCESS_KEY_ID'),\n            'secret' => env('AWS_SECRET_ACCESS_KEY'),\n            'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),\n            'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),\n            'endpoint' => env('DYNAMODB_ENDPOINT'),\n        ],\n\n        'octane' => [\n            'driver' => 'octane',\n        ],\n\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Cache Key Prefix\n    |--------------------------------------------------------------------------\n    |\n    | When utilizing the APC, database, memcached, Redis, or DynamoDB cache\n    | stores there might be other applications using the same cache. For\n    | that reason, you may prefix every cache key to avoid collisions.\n    |\n    */\n\n    'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),\n\n];\n"
  },
  {
    "path": "config/cors.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Cross-Origin Resource Sharing (CORS) Configuration\n    |--------------------------------------------------------------------------\n    |\n    | Here you may configure your settings for cross-origin resource sharing\n    | or \"CORS\". This determines what cross-origin operations may execute\n    | in web browsers. You are free to adjust these settings as needed.\n    |\n    | To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS\n    |\n    */\n\n    'paths' => ['api/*', 'sanctum/csrf-cookie'],\n\n    'allowed_methods' => ['*'],\n\n    'allowed_origins' => ['*'],\n\n    'allowed_origins_patterns' => [],\n\n    'allowed_headers' => ['*'],\n\n    'exposed_headers' => [],\n\n    'max_age' => 0,\n\n    'supports_credentials' => false,\n\n];\n"
  },
  {
    "path": "config/database.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Str;\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Default Database Connection Name\n    |--------------------------------------------------------------------------\n    |\n    | Here you may specify which of the database connections below you wish\n    | to use as your default connection for all database work. Of course\n    | you may use many connections at once using the Database library.\n    |\n    */\n\n    'default' => env('DB_CONNECTION', 'mysql'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Database Connections\n    |--------------------------------------------------------------------------\n    |\n    | Here are each of the database connections setup for your application.\n    | Of course, examples of configuring each database platform that is\n    | supported by Laravel is shown below to make development simple.\n    |\n    |\n    | All database work in Laravel is done through the PHP PDO facilities\n    | so make sure you have the driver for your particular database of\n    | choice installed on your machine before you begin development.\n    |\n    */\n\n    'connections' => [\n\n        'sqlite' => [\n            'driver' => 'sqlite',\n            'url' => env('DATABASE_URL'),\n            'database' => env('DB_DATABASE', database_path('database.sqlite')),\n            'prefix' => '',\n            'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),\n        ],\n\n        'mysql' => [\n            'driver' => 'mysql',\n            'url' => env('DATABASE_URL'),\n            'host' => env('DB_HOST', '127.0.0.1'),\n            'port' => env('DB_PORT', '3306'),\n            'database' => env('DB_DATABASE', 'forge'),\n            'username' => env('DB_USERNAME', 'forge'),\n            'password' => env('DB_PASSWORD', ''),\n            'unix_socket' => env('DB_SOCKET', ''),\n            'charset' => 'utf8mb4',\n            'collation' => 'utf8mb4_unicode_ci',\n            'prefix' => '',\n            'prefix_indexes' => true,\n            'strict' => true,\n            'engine' => null,\n            'options' => extension_loaded('pdo_mysql') ? array_filter([\n                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),\n            ]) : [],\n        ],\n\n        'pgsql' => [\n            'driver' => 'pgsql',\n            'url' => env('DATABASE_URL'),\n            'host' => env('DB_HOST', '127.0.0.1'),\n            'port' => env('DB_PORT', '5432'),\n            'database' => env('DB_DATABASE', 'forge'),\n            'username' => env('DB_USERNAME', 'forge'),\n            'password' => env('DB_PASSWORD', ''),\n            'charset' => 'utf8',\n            'prefix' => '',\n            'prefix_indexes' => true,\n            'search_path' => 'public',\n            'sslmode' => 'prefer',\n        ],\n\n        'sqlsrv' => [\n            'driver' => 'sqlsrv',\n            'url' => env('DATABASE_URL'),\n            'host' => env('DB_HOST', 'localhost'),\n            'port' => env('DB_PORT', '1433'),\n            'database' => env('DB_DATABASE', 'forge'),\n            'username' => env('DB_USERNAME', 'forge'),\n            'password' => env('DB_PASSWORD', ''),\n            'charset' => 'utf8',\n            'prefix' => '',\n            'prefix_indexes' => true,\n            // 'encrypt' => env('DB_ENCRYPT', 'yes'),\n            // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),\n        ],\n\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Migration Repository Table\n    |--------------------------------------------------------------------------\n    |\n    | This table keeps track of all the migrations that have already run for\n    | your application. Using this information, we can determine which of\n    | the migrations on disk haven't actually been run in the database.\n    |\n    */\n\n    'migrations' => 'migrations',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Redis Databases\n    |--------------------------------------------------------------------------\n    |\n    | Redis is an open source, fast, and advanced key-value store that also\n    | provides a richer body of commands than a typical key-value system\n    | such as APC or Memcached. Laravel makes it easy to dig right in.\n    |\n    */\n\n    'redis' => [\n\n        'client' => env('REDIS_CLIENT', 'phpredis'),\n\n        'options' => [\n            'cluster' => env('REDIS_CLUSTER', 'redis'),\n            'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),\n        ],\n\n        'default' => [\n            'url' => env('REDIS_URL'),\n            'host' => env('REDIS_HOST', '127.0.0.1'),\n            'username' => env('REDIS_USERNAME'),\n            'password' => env('REDIS_PASSWORD'),\n            'port' => env('REDIS_PORT', '6379'),\n            'database' => env('REDIS_DB', '0'),\n        ],\n\n        'cache' => [\n            'url' => env('REDIS_URL'),\n            'host' => env('REDIS_HOST', '127.0.0.1'),\n            'username' => env('REDIS_USERNAME'),\n            'password' => env('REDIS_PASSWORD'),\n            'port' => env('REDIS_PORT', '6379'),\n            'database' => env('REDIS_CACHE_DB', '1'),\n        ],\n\n    ],\n\n];\n"
  },
  {
    "path": "config/filesystems.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Default Filesystem Disk\n    |--------------------------------------------------------------------------\n    |\n    | Here you may specify the default filesystem disk that should be used\n    | by the framework. The \"local\" disk, as well as a variety of cloud\n    | based disks are available to your application. Just store away!\n    |\n    */\n\n    'default' => env('FILESYSTEM_DISK', 'local'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Filesystem Disks\n    |--------------------------------------------------------------------------\n    |\n    | Here you may configure as many filesystem \"disks\" as you wish, and you\n    | may even configure multiple disks of the same driver. Defaults have\n    | been set up for each driver as an example of the required values.\n    |\n    | Supported Drivers: \"local\", \"ftp\", \"sftp\", \"s3\"\n    |\n    */\n\n    'disks' => [\n\n        'local' => [\n            'driver' => 'local',\n            'root' => storage_path('app'),\n            'throw' => false,\n        ],\n\n        'public' => [\n            'driver' => 'local',\n            'root' => storage_path('app/public'),\n            'url' => env('APP_URL').'/storage',\n            'visibility' => 'public',\n            'throw' => false,\n        ],\n\n        's3' => [\n            'driver' => 's3',\n            'key' => env('AWS_ACCESS_KEY_ID'),\n            'secret' => env('AWS_SECRET_ACCESS_KEY'),\n            'region' => env('AWS_DEFAULT_REGION'),\n            'bucket' => env('AWS_BUCKET'),\n            'url' => env('AWS_URL'),\n            'endpoint' => env('AWS_ENDPOINT'),\n            'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),\n            'throw' => false,\n        ],\n\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Symbolic Links\n    |--------------------------------------------------------------------------\n    |\n    | Here you may configure the symbolic links that will be created when the\n    | `storage:link` Artisan command is executed. The array keys should be\n    | the locations of the links and the values should be their targets.\n    |\n    */\n\n    'links' => [\n        public_path('storage') => storage_path('app/public'),\n    ],\n\n];\n"
  },
  {
    "path": "config/fortify.php",
    "content": "<?php\n\nuse Laravel\\Fortify\\Features;\nuse Vigilant\\Users\\Http\\Middleware\\NoUserMiddleware;\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Fortify Guard\n    |--------------------------------------------------------------------------\n    |\n    | Here you may specify which authentication guard Fortify will use while\n    | authenticating users. This value should correspond with one of your\n    | guards that is already present in your \"auth\" configuration file.\n    |\n    */\n\n    'guard' => 'web',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Fortify Password Broker\n    |--------------------------------------------------------------------------\n    |\n    | Here you may specify which password broker Fortify can use when a user\n    | is resetting their password. This configured value should match one\n    | of your password brokers setup in your \"auth\" configuration file.\n    |\n    */\n\n    'passwords' => 'users',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Username / Email\n    |--------------------------------------------------------------------------\n    |\n    | This value defines which model attribute should be considered as your\n    | application's \"username\" field. Typically, this might be the email\n    | address of the users but you are free to change this value here.\n    |\n    | Out of the box, Fortify expects forgot password and reset password\n    | requests to have a field named 'email'. If the application uses\n    | another name for the field you may define it below as needed.\n    |\n    */\n\n    'username' => 'email',\n\n    'email' => 'email',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Lowercase Usernames\n    |--------------------------------------------------------------------------\n    |\n    | This value defines whether usernames should be lowercased before saving\n    | them in the database, as some database system string fields are case\n    | sensitive. You may disable this for your application if necessary.\n    |\n    */\n\n    'lowercase_usernames' => true,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Home Path\n    |--------------------------------------------------------------------------\n    |\n    | Here you may configure the path where users will get redirected during\n    | authentication or password reset when the operations are successful\n    | and the user is authenticated. You are free to change this value.\n    |\n    */\n\n    'home' => '/',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Fortify Routes Prefix / Subdomain\n    |--------------------------------------------------------------------------\n    |\n    | Here you may specify which prefix Fortify will assign to all the routes\n    | that it registers with the application. If necessary, you may change\n    | subdomain under which all of the Fortify routes will be available.\n    |\n    */\n\n    'prefix' => '',\n\n    'domain' => null,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Fortify Routes Middleware\n    |--------------------------------------------------------------------------\n    |\n    | Here you may specify which middleware Fortify will assign to the routes\n    | that it registers with the application. If necessary, you may change\n    | these middleware but typically this provided default is preferred.\n    |\n    */\n\n    'middleware' => ['web', NoUserMiddleware::class],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Rate Limiting\n    |--------------------------------------------------------------------------\n    |\n    | By default, Fortify will throttle logins to five requests per minute for\n    | every email and IP address combination. However, if you would like to\n    | specify a custom rate limiter to call then you may specify it here.\n    |\n    */\n\n    'limiters' => [\n        'login' => 'login',\n        'two-factor' => 'two-factor',\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Register View Routes\n    |--------------------------------------------------------------------------\n    |\n    | Here you may specify if the routes returning views should be disabled as\n    | you may not need them when building your own application. This may be\n    | especially true if you're writing a custom single-page application.\n    |\n    */\n\n    'views' => true,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Features\n    |--------------------------------------------------------------------------\n    |\n    | Some of the Fortify features are optional. You may disable the features\n    | by removing them from this array. You're free to only remove some of\n    | these features or you can even remove all of these if you need to.\n    |\n    */\n\n    'features' => [\n        Features::registration(),\n        Features::resetPasswords(),\n        Features::emailVerification(),\n        Features::updateProfileInformation(),\n        Features::updatePasswords(),\n        Features::twoFactorAuthentication([\n            'confirm' => true,\n            'confirmPassword' => true,\n            // 'window' => 0,\n        ]),\n    ],\n\n];\n"
  },
  {
    "path": "config/hashing.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Default Hash Driver\n    |--------------------------------------------------------------------------\n    |\n    | This option controls the default hash driver that will be used to hash\n    | passwords for your application. By default, the bcrypt algorithm is\n    | used; however, you remain free to modify this option if you wish.\n    |\n    | Supported: \"bcrypt\", \"argon\", \"argon2id\"\n    |\n    */\n\n    'driver' => 'bcrypt',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Bcrypt Options\n    |--------------------------------------------------------------------------\n    |\n    | Here you may specify the configuration options that should be used when\n    | passwords are hashed using the Bcrypt algorithm. This will allow you\n    | to control the amount of time it takes to hash the given password.\n    |\n    */\n\n    'bcrypt' => [\n        'rounds' => env('BCRYPT_ROUNDS', 12),\n        'verify' => true,\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Argon Options\n    |--------------------------------------------------------------------------\n    |\n    | Here you may specify the configuration options that should be used when\n    | passwords are hashed using the Argon algorithm. These will allow you\n    | to control the amount of time it takes to hash the given password.\n    |\n    */\n\n    'argon' => [\n        'memory' => 65536,\n        'threads' => 1,\n        'time' => 4,\n        'verify' => true,\n    ],\n\n];\n"
  },
  {
    "path": "config/horizon.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Str;\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Horizon Domain\n    |--------------------------------------------------------------------------\n    |\n    | This is the subdomain where Horizon will be accessible from. If this\n    | setting is null, Horizon will reside under the same domain as the\n    | application. Otherwise, this value will serve as the subdomain.\n    |\n    */\n\n    'domain' => env('HORIZON_DOMAIN'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Horizon Path\n    |--------------------------------------------------------------------------\n    |\n    | This is the URI path where Horizon will be accessible from. Feel free\n    | to change this path to anything you like. Note that the URI will not\n    | affect the paths of its internal API that aren't exposed to users.\n    |\n    */\n\n    'path' => env('HORIZON_PATH', 'horizon'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Horizon Redis Connection\n    |--------------------------------------------------------------------------\n    |\n    | This is the name of the Redis connection where Horizon will store the\n    | meta information required for it to function. It includes the list\n    | of supervisors, failed jobs, job metrics, and other information.\n    |\n    */\n\n    'use' => 'default',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Horizon Redis Prefix\n    |--------------------------------------------------------------------------\n    |\n    | This prefix will be used when storing all Horizon data in Redis. You\n    | may modify the prefix when you are running multiple installations\n    | of Horizon on the same server so that they don't have problems.\n    |\n    */\n\n    'prefix' => env(\n        'HORIZON_PREFIX',\n        Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'\n    ),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Horizon Route Middleware\n    |--------------------------------------------------------------------------\n    |\n    | These middleware will get attached onto each Horizon route, giving you\n    | the chance to add your own middleware to this list or change any of\n    | the existing middleware. Or, you can simply stick with this list.\n    |\n    */\n\n    'middleware' => ['web'],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Queue Wait Time Thresholds\n    |--------------------------------------------------------------------------\n    |\n    | This option allows you to configure when the LongWaitDetected event\n    | will be fired. Every connection / queue combination may have its\n    | own, unique threshold (in seconds) before this event is fired.\n    |\n    */\n\n    'waits' => [\n        'redis:default' => 60,\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Job Trimming Times\n    |--------------------------------------------------------------------------\n    |\n    | Here you can configure for how long (in minutes) you desire Horizon to\n    | persist the recent and failed jobs. Typically, recent jobs are kept\n    | for one hour while all failed jobs are stored for an entire week.\n    |\n    */\n\n    'trim' => [\n        'recent' => 1440,\n        'pending' => 1440,\n        'completed' => 60,\n        'recent_failed' => 10080,\n        'failed' => 10080,\n        'monitored' => 10080,\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Silenced Jobs\n    |--------------------------------------------------------------------------\n    |\n    | Silencing a job will instruct Horizon to not place the job in the list\n    | of completed jobs within the Horizon dashboard. This setting may be\n    | used to fully remove any noisy jobs from the completed jobs list.\n    |\n    */\n\n    'silenced' => [\n        // App\\Jobs\\ExampleJob::class,\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Metrics\n    |--------------------------------------------------------------------------\n    |\n    | Here you can configure how many snapshots should be kept to display in\n    | the metrics graph. This will get used in combination with Horizon's\n    | `horizon:snapshot` schedule to define how long to retain metrics.\n    |\n    */\n\n    'metrics' => [\n        'trim_snapshots' => [\n            'job' => 24,\n            'queue' => 24,\n        ],\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Fast Termination\n    |--------------------------------------------------------------------------\n    |\n    | When this option is enabled, Horizon's \"terminate\" command will not\n    | wait on all of the workers to terminate unless the --wait option\n    | is provided. Fast termination can shorten deployment delay by\n    | allowing a new instance of Horizon to start while the last\n    | instance will continue to terminate each of its workers.\n    |\n    */\n\n    'fast_termination' => false,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Memory Limit (MB)\n    |--------------------------------------------------------------------------\n    |\n    | This value describes the maximum amount of memory the Horizon master\n    | supervisor may consume before it is terminated and restarted. For\n    | configuring these limits on your workers, see the next section.\n    |\n    */\n\n    'memory_limit' => 64,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Queue Worker Configuration\n    |--------------------------------------------------------------------------\n    |\n    | Here you may define the queue worker settings used by your application\n    | in all environments. These supervisors and settings handle all your\n    | queued jobs and will be provisioned by Horizon during deployment.\n    |\n    */\n\n    'defaults' => [\n        'default' => [\n            'connection' => 'redis',\n            'queue' => ['default'],\n            'balance' => 'auto',\n            'autoScalingStrategy' => 'time',\n            'maxProcesses' => 1,\n            'maxTime' => 0,\n            'maxJobs' => 0,\n            'memory' => 128,\n            'tries' => 1,\n            'timeout' => 60,\n            'nice' => 0,\n        ],\n\n        'uptime' => [\n            'connection' => 'redis',\n            'queue' => ['uptime'],\n            'balance' => 'auto',\n            'autoScalingStrategy' => 'time',\n            'minProcesses' => 2,\n            'maxProcesses' => 4,\n            'maxTime' => 0,\n            'maxJobs' => 0,\n            'memory' => 128,\n            'tries' => 1,\n            'timeout' => 60,\n            'nice' => 0,\n        ],\n\n        'lighthouse' => [\n            'connection' => 'redis',\n            'queue' => ['lighthouse'],\n            'balance' => 'auto',\n            'autoScalingStrategy' => 'time',\n            'maxProcesses' => 1,\n            'maxTime' => 0,\n            'maxJobs' => 0,\n            'memory' => 128,\n            'tries' => 1,\n            'timeout' => 120,\n            'nice' => 0,\n        ],\n\n        'dns' => [\n            'connection' => 'redis',\n            'queue' => ['dns'],\n            'balance' => 'auto',\n            'autoScalingStrategy' => 'time',\n            'maxProcesses' => 1,\n            'maxTime' => 0,\n            'maxJobs' => 0,\n            'memory' => 128,\n            'tries' => 1,\n            'timeout' => 30,\n            'nice' => 0,\n        ],\n\n        'crawler' => [\n            'connection' => 'redis',\n            'queue' => ['crawler'],\n            'balance' => 'auto',\n            'autoScalingStrategy' => 'time',\n            'maxProcesses' => 5,\n            'maxTime' => 0,\n            'maxJobs' => 0,\n            'memory' => 128,\n            'tries' => 1,\n            'timeout' => 60,\n            'nice' => 0,\n        ],\n\n        'certificates' => [\n            'connection' => 'redis',\n            'queue' => ['certificates'],\n            'balance' => 'auto',\n            'autoScalingStrategy' => 'time',\n            'maxProcesses' => 5,\n            'maxTime' => 0,\n            'maxJobs' => 0,\n            'memory' => 128,\n            'tries' => 1,\n            'timeout' => 30,\n            'nice' => 0,\n        ],\n\n        'cve' => [\n            'connection' => 'redis',\n            'queue' => ['cve'],\n            'balance' => 'auto',\n            'autoScalingStrategy' => 'time',\n            'minProcesses' => 1,\n            'maxProcesses' => 2,\n            'maxTime' => 0,\n            'maxJobs' => 0,\n            'memory' => 512,\n            'tries' => 1,\n            'timeout' => 300,\n            'nice' => 0,\n        ],\n\n        'healthchecks' => [\n            'connection' => 'redis',\n            'queue' => ['healthchecks'],\n            'balance' => 'auto',\n            'autoScalingStrategy' => 'time',\n            'minProcesses' => 1,\n            'maxProcesses' => 4,\n            'maxTime' => 0,\n            'maxJobs' => 0,\n            'memory' => 512,\n            'tries' => 1,\n            'timeout' => 300,\n            'nice' => 0,\n        ],\n        'notifications' => [\n            'connection' => 'redis',\n            'queue' => ['notifications'],\n            'balance' => 'auto',\n            'autoScalingStrategy' => 'time',\n            'maxProcesses' => 2,\n            'maxTime' => 0,\n            'maxJobs' => 0,\n            'memory' => 128,\n            'tries' => 1,\n            'timeout' => 60,\n            'nice' => 0,\n        ],\n    ],\n\n    'environments' => [\n        'production' => [],\n        'test' => [],\n        'local' => [],\n    ],\n];\n"
  },
  {
    "path": "config/jetstream.php",
    "content": "<?php\n\nuse Laravel\\Jetstream\\Features;\nuse Laravel\\Jetstream\\Http\\Middleware\\AuthenticateSession;\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Jetstream Stack\n    |--------------------------------------------------------------------------\n    |\n    | This configuration value informs Jetstream which \"stack\" you will be\n    | using for your application. In general, this value is set for you\n    | during installation and will not need to be changed after that.\n    |\n    */\n\n    'stack' => 'livewire',\n\n    /*\n     |--------------------------------------------------------------------------\n     | Jetstream Route Middleware\n     |--------------------------------------------------------------------------\n     |\n     | Here you may specify which middleware Jetstream will assign to the routes\n     | that it registers with the application. When necessary, you may modify\n     | these middleware; however, this default value is usually sufficient.\n     |\n     */\n\n    'middleware' => ['web'],\n\n    'auth_session' => AuthenticateSession::class,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Jetstream Guard\n    |--------------------------------------------------------------------------\n    |\n    | Here you may specify the authentication guard Jetstream will use while\n    | authenticating users. This value should correspond with one of your\n    | guards that is already present in your \"auth\" configuration file.\n    |\n    */\n\n    'guard' => 'sanctum',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Features\n    |--------------------------------------------------------------------------\n    |\n    | Some of Jetstream's features are optional. You may disable the features\n    | by removing them from this array. You're free to only remove some of\n    | these features or you can even remove all of these if you need to.\n    |\n    */\n\n    'features' => [\n        // Features::termsAndPrivacyPolicy(),\n        // Features::profilePhotos(),\n        // Features::api(),\n        env('EDITION', 'ce') === 'ce' ? '' : Features::teams(['invitations' => true]),\n        Features::accountDeletion(),\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Profile Photo Disk\n    |--------------------------------------------------------------------------\n    |\n    | This configuration value determines the default disk that will be used\n    | when storing profile photos for your application's users. Typically\n    | this will be the \"public\" disk but you may adjust this if needed.\n    |\n    */\n\n    'profile_photo_disk' => 'public',\n\n];\n"
  },
  {
    "path": "config/livewire.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |---------------------------------------------------------------------------\n    | Class Namespace\n    |---------------------------------------------------------------------------\n    |\n    | This value sets the root class namespace for Livewire component classes in\n    | your application. This value will change where component auto-discovery\n    | finds components. It's also referenced by the file creation commands.\n    |\n    */\n\n    'class_namespace' => 'App\\\\Livewire',\n\n    /*\n    |---------------------------------------------------------------------------\n    | View Path\n    |---------------------------------------------------------------------------\n    |\n    | This value is used to specify where Livewire component Blade templates are\n    | stored when running file creation commands like `artisan make:livewire`.\n    | It is also used if you choose to omit a component's render() method.\n    |\n    */\n\n    'view_path' => resource_path('views/livewire'),\n\n    /*\n    |---------------------------------------------------------------------------\n    | Layout\n    |---------------------------------------------------------------------------\n    | The view that will be used as the layout when rendering a single component\n    | as an entire page via `Route::get('/post/create', CreatePost::class);`.\n    | In this case, the view returned by CreatePost will render into $slot.\n    |\n    */\n\n    'layout' => 'layouts.app',\n\n    /*\n    |---------------------------------------------------------------------------\n    | Lazy Loading Placeholder\n    |---------------------------------------------------------------------------\n    | Livewire allows you to lazy load components that would otherwise slow down\n    | the initial page load. Every component can have a custom placeholder or\n    | you can define the default placeholder view for all components below.\n    |\n    */\n\n    'lazy_placeholder' => null,\n\n    /*\n    |---------------------------------------------------------------------------\n    | Temporary File Uploads\n    |---------------------------------------------------------------------------\n    |\n    | Livewire handles file uploads by storing uploads in a temporary directory\n    | before the file is stored permanently. All file uploads are directed to\n    | a global endpoint for temporary storage. You may configure this below:\n    |\n    */\n\n    'temporary_file_upload' => [\n        'disk' => null,        // Example: 'local', 's3'              | Default: 'default'\n        'rules' => null,       // Example: ['file', 'mimes:png,jpg']  | Default: ['required', 'file', 'max:12288'] (12MB)\n        'directory' => null,   // Example: 'tmp'                      | Default: 'livewire-tmp'\n        'middleware' => null,  // Example: 'throttle:5,1'             | Default: 'throttle:60,1'\n        'preview_mimes' => [   // Supported file types for temporary pre-signed file URLs...\n            'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',\n            'mov', 'avi', 'wmv', 'mp3', 'm4a',\n            'jpg', 'jpeg', 'mpga', 'webp', 'wma',\n        ],\n        'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...\n    ],\n\n    /*\n    |---------------------------------------------------------------------------\n    | Render On Redirect\n    |---------------------------------------------------------------------------\n    |\n    | This value determines if Livewire will run a component's `render()` method\n    | after a redirect has been triggered using something like `redirect(...)`\n    | Setting this to true will render the view once more before redirecting\n    |\n    */\n\n    'render_on_redirect' => false,\n\n    /*\n    |---------------------------------------------------------------------------\n    | Eloquent Model Binding\n    |---------------------------------------------------------------------------\n    |\n    | Previous versions of Livewire supported binding directly to eloquent model\n    | properties using wire:model by default. However, this behavior has been\n    | deemed too \"magical\" and has therefore been put under a feature flag.\n    |\n    */\n\n    'legacy_model_binding' => false,\n\n    /*\n    |---------------------------------------------------------------------------\n    | Auto-inject Frontend Assets\n    |---------------------------------------------------------------------------\n    |\n    | By default, Livewire automatically injects its JavaScript and CSS into the\n    | <head> and <body> of pages containing Livewire components. By disabling\n    | this behavior, you need to use @livewireStyles and @livewireScripts.\n    |\n    */\n\n    'inject_assets' => true,\n\n    /*\n    |---------------------------------------------------------------------------\n    | Navigate (SPA mode)\n    |---------------------------------------------------------------------------\n    |\n    | By adding `wire:navigate` to links in your Livewire application, Livewire\n    | will prevent the default link handling and instead request those pages\n    | via AJAX, creating an SPA-like effect. Configure this behavior here.\n    |\n    */\n\n    'navigate' => [\n        'show_progress_bar' => true,\n        'progress_bar_color' => '#AF3029',\n    ],\n\n    /*\n    |---------------------------------------------------------------------------\n    | HTML Morph Markers\n    |---------------------------------------------------------------------------\n    |\n    | Livewire intelligently \"morphs\" existing HTML into the newly rendered HTML\n    | after each update. To make this process more reliable, Livewire injects\n    | \"markers\" into the rendered Blade surrounding @if, @class & @foreach.\n    |\n    */\n\n    'inject_morph_markers' => true,\n\n    /*\n    |---------------------------------------------------------------------------\n    | Pagination Theme\n    |---------------------------------------------------------------------------\n    |\n    | When enabling Livewire's pagination feature by using the `WithPagination`\n    | trait, Livewire will use Tailwind templates to render pagination views\n    | on the page. If you want Bootstrap CSS, you can specify: \"bootstrap\"\n    |\n    */\n\n    'pagination_theme' => 'tailwind',\n];\n"
  },
  {
    "path": "config/logging.php",
    "content": "<?php\n\nuse Monolog\\Handler\\NullHandler;\nuse Monolog\\Handler\\StreamHandler;\nuse Monolog\\Handler\\SyslogUdpHandler;\nuse Monolog\\Processor\\PsrLogMessageProcessor;\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Default Log Channel\n    |--------------------------------------------------------------------------\n    |\n    | This option defines the default log channel that gets used when writing\n    | messages to the logs. The name specified in this option should match\n    | one of the channels defined in the \"channels\" configuration array.\n    |\n    */\n\n    'default' => env('LOG_CHANNEL', 'daily'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Deprecations Log Channel\n    |--------------------------------------------------------------------------\n    |\n    | This option controls the log channel that should be used to log warnings\n    | regarding deprecated PHP and library features. This allows you to get\n    | your application ready for upcoming major versions of dependencies.\n    |\n    */\n\n    'deprecations' => [\n        'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),\n        'trace' => false,\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Log Channels\n    |--------------------------------------------------------------------------\n    |\n    | Here you may configure the log channels for your application. Out of\n    | the box, Laravel uses the Monolog PHP logging library. This gives\n    | you a variety of powerful log handlers / formatters to utilize.\n    |\n    | Available Drivers: \"single\", \"daily\", \"slack\", \"syslog\",\n    |                    \"errorlog\", \"monolog\",\n    |                    \"custom\", \"stack\"\n    |\n    */\n\n    'channels' => [\n        'stack' => [\n            'driver' => 'stack',\n            'channels' => ['single'],\n            'ignore_exceptions' => false,\n        ],\n\n        'single' => [\n            'driver' => 'single',\n            'path' => storage_path('logs/laravel.log'),\n            'level' => env('LOG_LEVEL', 'info'),\n            'replace_placeholders' => true,\n        ],\n\n        'daily' => [\n            'driver' => 'daily',\n            'path' => storage_path('logs/laravel.log'),\n            'level' => env('LOG_LEVEL', 'info'),\n            'days' => 14,\n            'replace_placeholders' => true,\n        ],\n\n        'slack' => [\n            'driver' => 'slack',\n            'url' => env('LOG_SLACK_WEBHOOK_URL'),\n            'username' => 'Laravel Log',\n            'emoji' => ':boom:',\n            'level' => env('LOG_LEVEL', 'critical'),\n            'replace_placeholders' => true,\n        ],\n\n        'papertrail' => [\n            'driver' => 'monolog',\n            'level' => env('LOG_LEVEL', 'debug'),\n            'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),\n            'handler_with' => [\n                'host' => env('PAPERTRAIL_URL'),\n                'port' => env('PAPERTRAIL_PORT'),\n                'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),\n            ],\n            'processors' => [PsrLogMessageProcessor::class],\n        ],\n\n        'stderr' => [\n            'driver' => 'monolog',\n            'level' => env('LOG_LEVEL', 'debug'),\n            'handler' => StreamHandler::class,\n            'formatter' => env('LOG_STDERR_FORMATTER'),\n            'with' => [\n                'stream' => 'php://stderr',\n            ],\n            'processors' => [PsrLogMessageProcessor::class],\n        ],\n\n        'syslog' => [\n            'driver' => 'syslog',\n            'level' => env('LOG_LEVEL', 'debug'),\n            'facility' => LOG_USER,\n            'replace_placeholders' => true,\n        ],\n\n        'errorlog' => [\n            'driver' => 'errorlog',\n            'level' => env('LOG_LEVEL', 'debug'),\n            'replace_placeholders' => true,\n        ],\n\n        'null' => [\n            'driver' => 'monolog',\n            'handler' => NullHandler::class,\n        ],\n\n        'emergency' => [\n            'path' => storage_path('logs/laravel.log'),\n        ],\n    ],\n\n];\n"
  },
  {
    "path": "config/mail.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Default Mailer\n    |--------------------------------------------------------------------------\n    |\n    | This option controls the default mailer that is used to send any email\n    | messages sent by your application. Alternative mailers may be setup\n    | and used as needed; however, this mailer will be used by default.\n    |\n    */\n\n    'default' => env('MAIL_MAILER', 'smtp'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Mailer Configurations\n    |--------------------------------------------------------------------------\n    |\n    | Here you may configure all of the mailers used by your application plus\n    | their respective settings. Several examples have been configured for\n    | you and you are free to add your own as your application requires.\n    |\n    | Laravel supports a variety of mail \"transport\" drivers to be used while\n    | sending an e-mail. You will specify which one you are using for your\n    | mailers below. You are free to add additional mailers as required.\n    |\n    | Supported: \"smtp\", \"sendmail\", \"mailgun\", \"ses\", \"ses-v2\",\n    |            \"postmark\", \"log\", \"array\", \"failover\", \"roundrobin\"\n    |\n    */\n\n    'mailers' => [\n        'smtp' => [\n            'transport' => 'smtp',\n            'url' => env('MAIL_URL'),\n            'host' => env('MAIL_HOST', 'smtp.mailgun.org'),\n            'port' => env('MAIL_PORT', 587),\n            'encryption' => env('MAIL_ENCRYPTION', 'tls'),\n            'username' => env('MAIL_USERNAME'),\n            'password' => env('MAIL_PASSWORD'),\n            'timeout' => null,\n            'local_domain' => env('MAIL_EHLO_DOMAIN'),\n            'verify_peer' => env('MAIL_VERIFY_PEER') === 'true',\n        ],\n\n        'ses' => [\n            'transport' => 'ses',\n        ],\n\n        'postmark' => [\n            'transport' => 'postmark',\n            // 'message_stream_id' => null,\n            // 'client' => [\n            //     'timeout' => 5,\n            // ],\n        ],\n\n        'mailgun' => [\n            'transport' => 'mailgun',\n            // 'client' => [\n            //     'timeout' => 5,\n            // ],\n        ],\n\n        'sendmail' => [\n            'transport' => 'sendmail',\n            'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),\n        ],\n\n        'log' => [\n            'transport' => 'log',\n            'channel' => env('MAIL_LOG_CHANNEL'),\n        ],\n\n        'array' => [\n            'transport' => 'array',\n        ],\n\n        'failover' => [\n            'transport' => 'failover',\n            'mailers' => [\n                'smtp',\n                'log',\n            ],\n        ],\n\n        'roundrobin' => [\n            'transport' => 'roundrobin',\n            'mailers' => [\n                'ses',\n                'postmark',\n            ],\n        ],\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Global \"From\" Address\n    |--------------------------------------------------------------------------\n    |\n    | You may wish for all e-mails sent by your application to be sent from\n    | the same address. Here, you may specify a name and address that is\n    | used globally for all e-mails that are sent by your application.\n    |\n    */\n\n    'from' => [\n        'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),\n        'name' => env('MAIL_FROM_NAME', 'Example'),\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Markdown Mail Settings\n    |--------------------------------------------------------------------------\n    |\n    | If you are using Markdown based email rendering, you may configure your\n    | theme and component paths here, allowing you to customize the design\n    | of the emails. Or, you may simply stick with the Laravel defaults!\n    |\n    */\n\n    'markdown' => [\n        'theme' => 'default',\n\n        'paths' => [\n            resource_path('views/vendor/mail'),\n        ],\n    ],\n\n];\n"
  },
  {
    "path": "config/queue.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Default Queue Connection Name\n    |--------------------------------------------------------------------------\n    |\n    | Laravel's queue API supports an assortment of back-ends via a single\n    | API, giving you convenient access to each back-end using the same\n    | syntax for every one. Here you may define a default connection.\n    |\n    */\n\n    'default' => env('QUEUE_CONNECTION', 'redis'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Queue Connections\n    |--------------------------------------------------------------------------\n    |\n    | Here you may configure the connection information for each server that\n    | is used by your application. A default configuration has been added\n    | for each back-end shipped with Laravel. You are free to add more.\n    |\n    | Drivers: \"sync\", \"database\", \"beanstalkd\", \"sqs\", \"redis\", \"null\"\n    |\n    */\n\n    'connections' => [\n\n        'sync' => [\n            'driver' => 'sync',\n        ],\n\n        'database' => [\n            'driver' => 'database',\n            'table' => 'jobs',\n            'queue' => 'default',\n            'retry_after' => 90,\n            'after_commit' => false,\n        ],\n\n        'beanstalkd' => [\n            'driver' => 'beanstalkd',\n            'host' => 'localhost',\n            'queue' => 'default',\n            'retry_after' => 90,\n            'block_for' => 0,\n            'after_commit' => false,\n        ],\n\n        'sqs' => [\n            'driver' => 'sqs',\n            'key' => env('AWS_ACCESS_KEY_ID'),\n            'secret' => env('AWS_SECRET_ACCESS_KEY'),\n            'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),\n            'queue' => env('SQS_QUEUE', 'default'),\n            'suffix' => env('SQS_SUFFIX'),\n            'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),\n            'after_commit' => false,\n        ],\n\n        'redis' => [\n            'driver' => 'redis',\n            'connection' => 'default',\n            'queue' => env('REDIS_QUEUE', 'default'),\n            'retry_after' => 3600,\n            'block_for' => null,\n            'after_commit' => false,\n        ],\n\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Job Batching\n    |--------------------------------------------------------------------------\n    |\n    | The following options configure the database and table that store job\n    | batching information. These options can be updated to any database\n    | connection and table which has been defined by your application.\n    |\n    */\n\n    'batching' => [\n        'database' => env('DB_CONNECTION', 'mysql'),\n        'table' => 'job_batches',\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Failed Queue Jobs\n    |--------------------------------------------------------------------------\n    |\n    | These options configure the behavior of failed queue job logging so you\n    | can control which database and table are used to store the jobs that\n    | have failed. You may change them to any database / table you wish.\n    |\n    */\n\n    'failed' => [\n        'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),\n        'database' => env('DB_CONNECTION', 'mysql'),\n        'table' => 'failed_jobs',\n    ],\n\n];\n"
  },
  {
    "path": "config/sanctum.php",
    "content": "<?php\n\nuse Laravel\\Sanctum\\Sanctum;\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Stateful Domains\n    |--------------------------------------------------------------------------\n    |\n    | Requests from the following domains / hosts will receive stateful API\n    | authentication cookies. Typically, these should include your local\n    | and production domains which access your API via a frontend SPA.\n    |\n    */\n\n    'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(\n        '%s%s',\n        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',\n        Sanctum::currentApplicationUrlWithPort()\n    ))),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Sanctum Guards\n    |--------------------------------------------------------------------------\n    |\n    | This array contains the authentication guards that will be checked when\n    | Sanctum is trying to authenticate a request. If none of these guards\n    | are able to authenticate the request, Sanctum will use the bearer\n    | token that's present on an incoming request for authentication.\n    |\n    */\n\n    'guard' => ['web'],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Expiration Minutes\n    |--------------------------------------------------------------------------\n    |\n    | This value controls the number of minutes until an issued token will be\n    | considered expired. This will override any values set in the token's\n    | \"expires_at\" attribute, but first-party sessions are not affected.\n    |\n    */\n\n    'expiration' => null,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Token Prefix\n    |--------------------------------------------------------------------------\n    |\n    | Sanctum can prefix new tokens in order to take advantage of numerous\n    | security scanning initiatives maintained by open source platforms\n    | that notify developers if they commit tokens into repositories.\n    |\n    | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning\n    |\n    */\n\n    'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Sanctum Middleware\n    |--------------------------------------------------------------------------\n    |\n    | When authenticating your first-party SPA with Sanctum you may need to\n    | customize some of the middleware Sanctum uses while processing the\n    | request. You may change the middleware listed below as required.\n    |\n    */\n\n    'middleware' => [\n        'authenticate_session' => Laravel\\Sanctum\\Http\\Middleware\\AuthenticateSession::class,\n        'encrypt_cookies' => App\\Http\\Middleware\\EncryptCookies::class,\n        'verify_csrf_token' => App\\Http\\Middleware\\VerifyCsrfToken::class,\n    ],\n\n];\n"
  },
  {
    "path": "config/services.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Third Party Services\n    |--------------------------------------------------------------------------\n    |\n    | This file is for storing the credentials for third party services such\n    | as Mailgun, Postmark, AWS and more. This file provides the de facto\n    | location for this type of information, allowing packages to have\n    | a conventional file to locate the various service credentials.\n    |\n    */\n\n    'mailgun' => [\n        'domain' => env('MAILGUN_DOMAIN'),\n        'secret' => env('MAILGUN_SECRET'),\n        'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),\n        'scheme' => 'https',\n    ],\n\n    'postmark' => [\n        'token' => env('POSTMARK_TOKEN'),\n    ],\n\n    'ses' => [\n        'key' => env('AWS_ACCESS_KEY_ID'),\n        'secret' => env('AWS_SECRET_ACCESS_KEY'),\n        'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),\n    ],\n\n    'google' => [\n        'enabled' => env('GOOGLE_LOGIN_ENABLED', false),\n        'client_id' => env('GOOGLE_CLIENT_ID'),\n        'client_secret' => env('GOOGLE_CLIENT_SECRET'),\n        'redirect' => env('GOOGLE_REDIRECT_URI'),\n    ],\n];\n"
  },
  {
    "path": "config/session.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Str;\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Default Session Driver\n    |--------------------------------------------------------------------------\n    |\n    | This option controls the default session \"driver\" that will be used on\n    | requests. By default, we will use the lightweight native driver but\n    | you may specify any of the other wonderful drivers provided here.\n    |\n    | Supported: \"file\", \"cookie\", \"database\", \"apc\",\n    |            \"memcached\", \"redis\", \"dynamodb\", \"array\"\n    |\n    */\n\n    'driver' => env('SESSION_DRIVER', 'database'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Session Lifetime\n    |--------------------------------------------------------------------------\n    |\n    | Here you may specify the number of minutes that you wish the session\n    | to be allowed to remain idle before it expires. If you want them\n    | to immediately expire on the browser closing, set that option.\n    |\n    */\n\n    'lifetime' => env('SESSION_LIFETIME', 1440),\n\n    'expire_on_close' => false,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Session Encryption\n    |--------------------------------------------------------------------------\n    |\n    | This option allows you to easily specify that all of your session data\n    | should be encrypted before it is stored. All encryption will be run\n    | automatically by Laravel and you can use the Session like normal.\n    |\n    */\n\n    'encrypt' => false,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Session File Location\n    |--------------------------------------------------------------------------\n    |\n    | When using the native session driver, we need a location where session\n    | files may be stored. A default has been set for you but a different\n    | location may be specified. This is only needed for file sessions.\n    |\n    */\n\n    'files' => storage_path('framework/sessions'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Session Database Connection\n    |--------------------------------------------------------------------------\n    |\n    | When using the \"database\" or \"redis\" session drivers, you may specify a\n    | connection that should be used to manage these sessions. This should\n    | correspond to a connection in your database configuration options.\n    |\n    */\n\n    'connection' => env('SESSION_CONNECTION'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Session Database Table\n    |--------------------------------------------------------------------------\n    |\n    | When using the \"database\" session driver, you may specify the table we\n    | should use to manage the sessions. Of course, a sensible default is\n    | provided for you; however, you are free to change this as needed.\n    |\n    */\n\n    'table' => 'sessions',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Session Cache Store\n    |--------------------------------------------------------------------------\n    |\n    | While using one of the framework's cache driven session backends you may\n    | list a cache store that should be used for these sessions. This value\n    | must match with one of the application's configured cache \"stores\".\n    |\n    | Affects: \"apc\", \"dynamodb\", \"memcached\", \"redis\"\n    |\n    */\n\n    'store' => env('SESSION_STORE'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Session Sweeping Lottery\n    |--------------------------------------------------------------------------\n    |\n    | Some session drivers must manually sweep their storage location to get\n    | rid of old sessions from storage. Here are the chances that it will\n    | happen on a given request. By default, the odds are 2 out of 100.\n    |\n    */\n\n    'lottery' => [2, 100],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Session Cookie Name\n    |--------------------------------------------------------------------------\n    |\n    | Here you may change the name of the cookie used to identify a session\n    | instance by ID. The name specified here will get used every time a\n    | new session cookie is created by the framework for every driver.\n    |\n    */\n\n    'cookie' => env(\n        'SESSION_COOKIE',\n        Str::slug(env('APP_NAME', 'laravel'), '_').'_session'\n    ),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Session Cookie Path\n    |--------------------------------------------------------------------------\n    |\n    | The session cookie path determines the path for which the cookie will\n    | be regarded as available. Typically, this will be the root path of\n    | your application but you are free to change this when necessary.\n    |\n    */\n\n    'path' => '/',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Session Cookie Domain\n    |--------------------------------------------------------------------------\n    |\n    | Here you may change the domain of the cookie used to identify a session\n    | in your application. This will determine which domains the cookie is\n    | available to in your application. A sensible default has been set.\n    |\n    */\n\n    'domain' => env('SESSION_DOMAIN'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | HTTPS Only Cookies\n    |--------------------------------------------------------------------------\n    |\n    | By setting this option to true, session cookies will only be sent back\n    | to the server if the browser has a HTTPS connection. This will keep\n    | the cookie from being sent to you when it can't be done securely.\n    |\n    */\n\n    'secure' => env('SESSION_SECURE_COOKIE'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | HTTP Access Only\n    |--------------------------------------------------------------------------\n    |\n    | Setting this value to true will prevent JavaScript from accessing the\n    | value of the cookie and the cookie will only be accessible through\n    | the HTTP protocol. You are free to modify this option if needed.\n    |\n    */\n\n    'http_only' => true,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Same-Site Cookies\n    |--------------------------------------------------------------------------\n    |\n    | This option determines how your cookies behave when cross-site requests\n    | take place, and can be used to mitigate CSRF attacks. By default, we\n    | will set this value to \"lax\" since this is a secure default value.\n    |\n    | Supported: \"lax\", \"strict\", \"none\", null\n    |\n    */\n\n    'same_site' => 'lax',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Partitioned Cookies\n    |--------------------------------------------------------------------------\n    |\n    | Setting this value to true will tie the cookie to the top-level site for\n    | a cross-site context. Partitioned cookies are accepted by the browser\n    | when flagged \"secure\" and the Same-Site attribute is set to \"none\".\n    |\n    */\n\n    'partitioned' => false,\n\n];\n"
  },
  {
    "path": "config/view.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | View Storage Paths\n    |--------------------------------------------------------------------------\n    |\n    | Most templating systems load templates from disk. Here you may specify\n    | an array of paths that should be checked for your views. Of course\n    | the usual Laravel view path has already been registered for you.\n    |\n    */\n\n    'paths' => [\n        resource_path('views'),\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Compiled View Path\n    |--------------------------------------------------------------------------\n    |\n    | This option determines where all the compiled Blade templates will be\n    | stored for your application. Typically, this is within the storage\n    | directory. However, as usual, you are free to change this value.\n    |\n    */\n\n    'compiled' => env(\n        'VIEW_COMPILED_PATH',\n        realpath(storage_path('framework/views'))\n    ),\n\n];\n"
  },
  {
    "path": "database/.gitignore",
    "content": "*.sqlite*\n"
  },
  {
    "path": "database/migrations/2019_08_19_000000_create_failed_jobs_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('failed_jobs', function (Blueprint $table) {\n            $table->id();\n            $table->string('uuid')->unique();\n            $table->text('connection');\n            $table->text('queue');\n            $table->longText('payload');\n            $table->longText('exception');\n            $table->timestamp('failed_at')->useCurrent();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('failed_jobs');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('personal_access_tokens', function (Blueprint $table) {\n            $table->id();\n            $table->morphs('tokenable');\n            $table->string('name');\n            $table->string('token', 64)->unique();\n            $table->text('abilities')->nullable();\n            $table->timestamp('last_used_at')->nullable();\n            $table->timestamp('expires_at')->nullable();\n            $table->timestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('personal_access_tokens');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2024_02_18_184745_create_sessions_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('sessions', function (Blueprint $table) {\n            $table->string('id')->primary();\n            $table->foreignId('user_id')->nullable()->index();\n            $table->string('ip_address', 45)->nullable();\n            $table->text('user_agent')->nullable();\n            $table->longText('payload');\n            $table->integer('last_activity')->index();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('sessions');\n    }\n};\n"
  },
  {
    "path": "database/migrations/2024_03_23_092656_create_notifications_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('notifications', function (Blueprint $table) {\n            $table->uuid('id')->primary();\n            $table->string('type');\n            $table->morphs('notifiable');\n            $table->text('data');\n            $table->timestamp('read_at')->nullable();\n            $table->timestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('notifications');\n    }\n};\n"
  },
  {
    "path": "database/seeders/DatabaseSeeder.php",
    "content": "<?php\n\nnamespace Database\\Seeders;\n\n// use Illuminate\\Database\\Console\\Seeds\\WithoutModelEvents;\nuse Illuminate\\Database\\Seeder;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass DatabaseSeeder extends Seeder\n{\n    /**\n     * Seed the application's database.\n     */\n    public function run(): void\n    {\n        /** @var Monitor $monitor */\n        $monitor = Monitor::factory()->create();\n\n        $latencyMin = 5;\n        $latencyMax = 20;\n\n        $currentDate = now();\n\n        for ($i = 0; $i < 72; $i++) {\n\n            $currentDate->subHour();\n\n            $monitor->aggregatedResults()->create([\n                'total_time' => rand($latencyMin, $latencyMax),\n                'created_at' => $currentDate,\n                'updated_at' => $currentDate,\n            ]);\n\n        }\n\n    }\n}\n"
  },
  {
    "path": "docker/crontab",
    "content": "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /app/storage/logs/schedule.log 2>&1\n"
  },
  {
    "path": "docker/entrypoint.sh",
    "content": "#!/bin/sh\n\ncp -f -r /tmp/public/* /app/public\n\nmkdir -p /app/storage/framework/cache\nmkdir -p /app/storage/framework/sessions\nmkdir -p /app/storage/framework/views\nmkdir -p /app/storage/logs\nchown -R www-data:www-data /app/storage\n\nif ! grep -q \"^APP_KEY=\" \".env\" || [ -z \"$(grep \"^APP_KEY=\" \".env\" | cut -d '=' -f2)\" ]; then\n    php artisan key:generate\nfi\n\nphp artisan optimize:clear\nphp artisan migrate --force\nphp artisan storage:link\nphp artisan notifications:create\nphp artisan notifications:rename-classes\nphp artisan optimize\n\n/usr/bin/supervisord -c /app/docker/supervisor/supervisor.conf\n"
  },
  {
    "path": "docker/horizon-entrypoint.sh",
    "content": "#!/bin/sh\n\nmkdir -p /app/storage/framework/cache\nmkdir -p /app/storage/framework/sessions\nmkdir -p /app/storage/framework/views\nmkdir -p /app/storage/logs\nchown -R www-data:www-data /app/storage\n\nexec su-exec www-data php artisan horizon\n"
  },
  {
    "path": "docker/nginx.conf",
    "content": "server {\n    listen 8000;\n    server_name _;\n\n    root /app/public;\n    index index.php;\n\n    access_log /app/storage/logs/nginx-access.log;\n    error_log /app/storage/logs/nginx-error.log;\n\n    sendfile on;\n    tcp_nopush on;\n    tcp_nodelay on;\n    keepalive_timeout 30;\n    keepalive_requests 1000;\n\n    gzip on;\n    gzip_comp_level 5;\n    gzip_min_length 256;\n    gzip_proxied any;\n    gzip_vary on;\n    gzip_types\n        application/javascript\n        application/json\n        application/xml\n        application/rss+xml\n        text/css\n        text/javascript\n        text/plain\n        text/xml\n        image/svg+xml;\n\n    location ~* \\.(css|js|ico|gif|jpe?g|png|webp|avif|svg|woff2?|ttf|eot|otf)$ {\n        expires 30d;\n        access_log off;\n        add_header Cache-Control \"public, immutable\";\n        try_files $uri /index.php?$query_string;\n    }\n\n    location / {\n        try_files $uri $uri/ /index.php?$query_string;\n    }\n\n    location = /index.php {\n        fastcgi_pass unix:/run/php-fpm.sock;\n        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n        include fastcgi_params;\n        fastcgi_read_timeout 300;\n\n        fastcgi_buffering on;\n        fastcgi_buffer_size 16k;\n        fastcgi_buffers 16 16k;\n        fastcgi_busy_buffers_size 32k;\n    }\n\n    location ~ \\.php$ {\n        return 404;\n    }\n\n    location ~ /\\.ht {\n        deny all;\n    }\n}\n"
  },
  {
    "path": "docker/php-fpm.ini",
    "content": "max_execution_time = 300\nmax_input_time = 60\nmemory_limit = 256M\n\nexpose_php = Off\ndisplay_errors = Off\nlog_errors = On\nerror_log = /app/storage/logs/php-errors.log\nerror_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT\n\nopcache.enable = 1\nopcache.memory_consumption = 256\nopcache.interned_strings_buffer = 16\nopcache.max_accelerated_files = 20000\nopcache.validate_timestamps = 0\nopcache.save_comments = 1\nopcache.enable_file_override = 1\nopcache.preload = /app/docker/preload.php\nopcache.preload_user = www-data\nopcache.jit_buffer_size = 64M\nopcache.jit = 1255\n\nrealpath_cache_size = 4096K\nrealpath_cache_ttl = 600\n"
  },
  {
    "path": "docker/preload.php",
    "content": "<?php\n\nrequire_once '/app/vendor/autoload.php';\n\n$files = array_merge(\n    glob('/app/vendor/laravel/framework/src/Illuminate/Support/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/Support/Facades/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/Collections/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/Http/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/Routing/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/Database/Query/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/Container/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/Pipeline/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/View/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/Cache/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/Session/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/Auth/*.php') ?: [],\n    glob('/app/vendor/laravel/framework/src/Illuminate/Validation/*.php') ?: [],\n\n    glob('/app/app/Models/*.php') ?: [],\n\n    glob('/app/packages/*/src/Models/*.php') ?: [],\n    glob('/app/packages/*/src/Actions/*.php') ?: [],\n    glob('/app/packages/*/src/Enums/*.php') ?: [],\n    glob('/app/packages/*/src/Data/*.php') ?: [],\n    glob('/app/packages/*/src/Contracts/*.php') ?: [],\n    glob('/app/packages/*/src/Concerns/*.php') ?: [],\n    glob('/app/packages/*/src/Http/Controllers/*.php') ?: [],\n    glob('/app/packages/*/src/Http/Resources/*.php') ?: [],\n    glob('/app/packages/*/src/Http/Requests/*.php') ?: [],\n    glob('/app/packages/*/src/Livewire/*.php') ?: [],\n    glob('/app/packages/*/src/Scopes/*.php') ?: [],\n    glob('/app/packages/*/src/Observers/*.php') ?: [],\n\n    glob('/app/packages/saas/packages/*/src/Models/*.php') ?: [],\n    glob('/app/packages/saas/packages/*/src/Actions/*.php') ?: [],\n    glob('/app/packages/saas/packages/*/src/Enums/*.php') ?: [],\n    glob('/app/packages/saas/packages/*/src/Data/*.php') ?: [],\n    glob('/app/packages/saas/packages/*/src/Contracts/*.php') ?: [],\n    glob('/app/packages/saas/packages/*/src/Concerns/*.php') ?: [],\n    glob('/app/packages/saas/packages/*/src/Http/Controllers/*.php') ?: [],\n    glob('/app/packages/saas/packages/*/src/Http/Resources/*.php') ?: [],\n    glob('/app/packages/saas/packages/*/src/Http/Requests/*.php') ?: [],\n    glob('/app/packages/saas/packages/*/src/Livewire/*.php') ?: [],\n    glob('/app/packages/saas/packages/*/src/Scopes/*.php') ?: [],\n    glob('/app/packages/saas/packages/*/src/Observers/*.php') ?: [],\n);\n\nforeach ($files as $file) {\n    try {\n        if (is_file($file)) {\n            opcache_compile_file($file);\n        }\n    } catch (Throwable) {\n        //\n    }\n}\n"
  },
  {
    "path": "docker/supervisor/supervisor.conf",
    "content": "[supervisord]\nnodaemon=true\nuser=root\nlogfile=/app/storage/logs/supervisord.log\nlogfile_maxbytes=10MB\n\n[program:cron]\ncommand=/usr/sbin/crond -f -l 5\nstdout_logfile=/app/storage/logs/cron.log\nstderr_logfile=/app/storage/logs/cron-error.log\nstdout_logfile_maxbytes=10MB\nstderr_logfile_maxbytes=10MB\nautorestart=true\n\n[program:php-fpm]\ncommand=php-fpm -F\nstdout_logfile=/app/storage/logs/php-fpm.log\nredirect_stderr=true\nstdout_logfile_maxbytes=10MB\nstdout_logfile_backups=5\nautorestart=true\n\n[program:nginx]\ncommand=nginx -g \"daemon off;\"\nstdout_logfile=/app/storage/logs/nginx.log\nredirect_stderr=true\nstdout_logfile_maxbytes=10MB\nstdout_logfile_backups=5\nautorestart=true\n"
  },
  {
    "path": "docker/www.conf",
    "content": "[www]\nuser = www-data\ngroup = www-data\n\nlisten = /run/php-fpm.sock\nlisten.owner = www-data\nlisten.group = nginx\nlisten.mode = 0666\n\npm = dynamic\npm.max_children = 24\npm.start_servers = 4\npm.min_spare_servers = 2\npm.max_spare_servers = 6\npm.max_requests = 1000\n\nrequest_terminate_timeout = 300\n\nslowlog = /app/storage/logs/php-fpm-slow.log\nrequest_slowlog_timeout = 5\n\nclear_env = no\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n    app:\n        image: ghcr.io/govigilant/vigilant:latest\n        build:\n            context: .\n        volumes:\n            - type: bind\n              source: ./.env\n              target: /app/.env\n            - ./storage:/app/storage\n            - public:/app/public\n        restart: always\n        working_dir: /app\n        networks:\n            - vigilant\n        healthcheck:\n            test: curl --fail http://localhost:8000 || exit 1\n            interval: 30s\n            timeout: 10s\n            retries: 5\n        depends_on:\n            mysql:\n                condition: service_healthy\n        ports:\n            - \"8000:8000\"\n\n    horizon:\n        image: ghcr.io/govigilant/vigilant:latest\n        volumes:\n            - type: bind\n              source: ./.env\n              target: /app/.env\n              read_only: true\n            - ./storage:/app/storage\n            - public:/app/public\n        restart: always\n        working_dir: /app\n        networks:\n            - vigilant\n        entrypoint: [\"sh\", \"/app/docker/horizon-entrypoint.sh\"]\n        healthcheck:\n            test: [\"CMD\", \"php\", \"artisan\", \"horizon:status\"]\n            interval: 30s\n            timeout: 10s\n            start_period: 10s\n            retries: 3\n        depends_on:\n            mysql:\n                condition: service_healthy\n            redis:\n                condition: service_healthy\n\n    mysql:\n        image: mysql:8.0\n        restart: always\n        environment:\n            - MYSQL_DATABASE=vigilant\n            - MYSQL_ROOT_PASSWORD=password\n        volumes:\n            - database:/var/lib/mysql\n        networks:\n            - vigilant\n        healthcheck:\n            test: [\"CMD\", \"mysqladmin\", \"ping\", \"-h\", \"localhost\"]\n            interval: 10s\n            timeout: 20s\n            retries: 10\n\n    redis:\n        image: redis:7\n        restart: always\n        volumes:\n            - redis:/data\n        networks:\n            - vigilant\n        healthcheck:\n            test: [\"CMD\", \"redis-cli\", \"ping\"]\n\n    lighthouse:\n        image: ghcr.io/govigilant/lighthouse-server:latest\n        restart: always\n        deploy:\n            resources:\n                reservations:\n                    memory: 2G\n        networks:\n            - vigilant\n\n    outpost:\n        image: ghcr.io/govigilant/vigilant-uptime-outpost:latest\n        restart: always\n        environment:\n            - VIGILANT_URL=http://app:8000\n            - OUTPOST_SECRET=outpost-secret\n        depends_on:\n            app:\n                condition: service_healthy\n        networks:\n            - vigilant\n\nnetworks:\n    vigilant:\n\nvolumes:\n    public:\n    database:\n    redis:\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"private\": true,\n    \"type\": \"module\",\n    \"scripts\": {\n        \"predev\": \"node scripts/generate-tailwind-sources.mjs\",\n        \"dev\": \"vite\",\n        \"prebuild\": \"find packages -type d -name vendor -exec rm -r {} + && node scripts/generate-tailwind-sources.mjs\",\n        \"build\": \"NODE_OPTIONS='--max-old-space-size=6144' vite build\"\n    },\n    \"devDependencies\": {\n        \"@tailwindcss/forms\": \"^0.5.2\",\n        \"@tailwindcss/postcss\": \"^4.1.3\",\n        \"@tailwindcss/typography\": \"^0.5.0\",\n        \"axios\": \"1.6.4\",\n        \"laravel-vite-plugin\": \"^1.0.0\",\n        \"postcss\": \"^8.4.14\",\n        \"tailwindcss\": \"^4.1.3\",\n        \"vite\": \"6.4.1\"\n    },\n    \"dependencies\": {\n        \"chart.js\": \"^4.4.1\"\n    }\n}\n"
  },
  {
    "path": "packages/certificates/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/certificates/composer.json",
    "content": "{\n    \"name\": \"vigilant/certificates\",\n    \"description\": \"Vigilant Certificate Monitor\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"guzzlehttp/guzzle\": \"^7.8\",\n        \"laravel/framework\": \"^12.0\",\n        \"livewire/livewire\": \"^3.4\",\n        \"vigilant/core\": \"@dev\",\n        \"vigilant/sites\": \"@dev\",\n        \"vigilant/users\": \"@dev\",\n        \"vigilant/frontend\": \"@dev\",\n        \"vigilant/notifications\": \"@dev\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Certificates\\\\\": \"src\",\n            \"Vigilant\\\\Certificates\\\\Database\\\\Factories\\\\\": \"database/factories\",\n            \"Vigilant\\\\Users\\\\Database\\\\Factories\\\\\": \"../users/database/factories\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Certificates\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\Certificates\\\\ServiceProvider\"\n            ]\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/certificates/config/certificates.php",
    "content": "<?php\n\nreturn [\n    'queue' => 'certificates',\n];\n"
  },
  {
    "path": "packages/certificates/database/migrations/2025_04_08_200000_create_create_certificate_monitors_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('certificate_monitors', function (Blueprint $table): void {\n            $table->id();\n            $table->foreignIdFor(Site::class)->nullable()->constrained()->onDelete('cascade');\n            $table->foreignIdFor(Team::class)->constrained()->onDelete('cascade');\n            $table->boolean('enabled')->default(true);\n\n            $table->dateTime('next_check')->nullable();\n\n            $table->string('domain');\n            $table->integer('port')->default(443);\n            $table->string('serial_number')->nullable();\n            $table->string('protocol')->nullable();\n            $table->string('fingerprint')->nullable();\n\n            $table->dateTime('valid_from')->nullable();\n            $table->dateTime('valid_to')->nullable();\n\n            $table->json('data')->nullable();\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('certificate_monitors');\n    }\n};\n"
  },
  {
    "path": "packages/certificates/database/migrations/2025_04_12_090000_create_create_certificate_monitor_history_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('certificate_monitor_histories', function (Blueprint $table): void {\n            $table->id();\n            $table->foreignIdFor(CertificateMonitor::class)->nullable()->constrained()->onDelete('cascade');\n            $table->foreignIdFor(Team::class)->constrained()->onDelete('cascade');\n\n            $table->string('serial_number')->nullable();\n            $table->string('protocol')->nullable();\n            $table->string('fingerprint')->nullable();\n\n            $table->dateTime('valid_from')->nullable();\n            $table->dateTime('valid_to')->nullable();\n\n            $table->json('data')->nullable();\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('certificate_monitor_histories');\n    }\n};\n"
  },
  {
    "path": "packages/certificates/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n    ignoreErrors:\n        - identifier: missingType.iterableValue\n        - identifier: missingType.generics\n"
  },
  {
    "path": "packages/certificates/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/certificates/resources/navigation.php",
    "content": "<?php\n\nuse Vigilant\\Core\\Facades\\Navigation;\n\nNavigation::add(route('certificates'), 'Certificates')\n    ->parent('infrastructure')\n    ->icon('phosphor-certificate')\n    ->gate('use-certificates')\n    ->routeIs('certificate*')\n    ->sort(600);\n"
  },
  {
    "path": "packages/certificates/resources/views/components/empty-states/monitors.blade.php",
    "content": "<x-frontend::empty-state\n    :title=\"__('No Certificate Monitors')\"\n    :description=\"__('Track TLS certificate expirations and changes by adding your first certificate monitor.')\"\n    icon=\"phosphor-warning-circle\"\n    iconClass=\"h-12 w-12 text-teal\"\n    iconWrapperClass=\"rounded-full bg-teal/10 p-4 mb-6\"\n    :buttonHref=\"route('certificates.create')\"\n    :buttonText=\"__('Add Certificate Monitor')\"\n    buttonClass=\"bg-gradient-to-r from-teal via-cyan to-teal bg-300% text-base-50 px-5 py-2.5 rounded-lg hover:shadow-lg hover:shadow-teal/30 transition-all duration-300\"\n/>\n"
  },
  {
    "path": "packages/certificates/resources/views/index.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header title=\"Certificate Monitoring\">\n            <x-frontend::page-header.actions>\n                <x-create-button dusk=\"certificate-add-button\" :href=\"route('certificates.create')\"\n                    model=\"Vigilant\\Certificates\\Models\\CertificateMonitor\">\n                    @lang('Add Certificate Monitor')\n                </x-create-button>\n            </x-frontend::page-header.actions>\n            <x-frontend::page-header.mobile-actions>\n                <x-create-button-dropdown :href=\"route('certificates.create')\" model=\"Vigilant\\Certificates\\Models\\CertificateMonitor\">\n                    @lang('Add Certificate Monitor')\n                </x-create-button-dropdown>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    @if ($hasMonitors)\n        <livewire:certificate-monitor-table />\n    @else\n        <x-certificates::empty-states.monitors />\n    @endif\n</x-app-layout>\n"
  },
  {
    "path": "packages/certificates/resources/views/livewire/certificate-monitor-form.blade.php",
    "content": "<div>\n    @if (!$inline)\n        <x-slot name=\"header\">\n            <x-page-header :title=\"$updating ? 'Edit Certificate Monitor - ' . $certificateMonitor->url : 'Add Certificate Monitor'\" :back=\"$updating\n                ? route('certificates.index', ['monitor' => $certificateMonitor])\n                : route('certificates')\">\n            </x-page-header>\n        </x-slot>\n    @endif\n\n    <form wire:submit=\"save\">\n        <div class=\"max-w-7xl mx-auto\">\n            <x-card>\n                <div class=\"flex flex-col gap-4\">\n                    @if (!$inline)\n                        <x-form.checkbox field=\"form.enabled\" name=\"Enabled\"\n                            description=\"Enable or disable this certificate monitor\" />\n                    @endif\n                    <x-form.text field=\"form.domain\" name=\"Domain\" description=\"Domain\" />\n\n                    <x-form.number field=\"form.port\" name=\"Port\" description=\"Port\" />\n\n                    @if (!$inline)\n                        <x-form.submit-button dusk=\"submit-button\" :submitText=\"$updating ? 'Save' : 'Create'\" />\n                    @endif\n                </div>\n            </x-card>\n        </div>\n    </form>\n</div>\n"
  },
  {
    "path": "packages/certificates/resources/views/livewire/monitor/dashboard.blade.php",
    "content": "<div class=\"\">\n\n    <dl class=\"grid grid-cols-2 lg:grid-cols-4 gap-4\">\n        <x-frontend::stats-card :title=\"__('Expiry')\">\n            @if ($monitor->valid_to === null)\n                <span class=\"text-red-light\">@lang('Unknown')</span>\n            @elseif ($monitor->valid_to->isPast())\n                <span class=\"text-red-light\">@lang('Expired :diff ago', ['diff' => $monitor->valid_to->longAbsoluteDiffForHumans()])</span>\n            @else\n                <span class=\"text-green\">@lang('Expires in :diff', ['diff' => $monitor->valid_to->longAbsoluteDiffForHumans()])</span>\n            @endif\n        </x-frontend::stats-card>\n        <x-frontend::stats-card :title=\"__('Valid from')\">\n            @if ($monitor->valid_from === null)\n                <span class=\"text-red-light\">@lang('Unknown')</span>\n            @else\n                <span class=\"text-base-200\">{{ $monitor->valid_from->toDatetimeString() }}</span>\n            @endif\n        </x-frontend::stats-card>\n\n        <x-frontend::stats-card :title=\"__('Valid to')\">\n            @if ($monitor->valid_to === null)\n                <span class=\"text-red-light\">@lang('Unknown')</span>\n            @else\n                <span class=\"text-base-200\">{{ $monitor->valid_to->toDatetimeString() }}</span>\n            @endif\n        </x-frontend::stats-card>\n\n        <x-frontend::stats-card :title=\"__('Issuer')\">\n            @if (data_get($monitor->data ?? [], 'issuer.CN') === null)\n                <span class=\"text-red-light\">@lang('Unknown')</span>\n            @else\n                <span class=\"text-base-200\">{{ data_get($monitor->data, 'issuer.CN') }}</span>\n            @endif\n        </x-frontend::stats-card>\n    </dl>\n</div>\n"
  },
  {
    "path": "packages/certificates/resources/views/monitor/index.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header :back=\"route('certificates')\" :title=\"'Certificate Monitor - ' . $monitor->domain . ($monitor->enabled ? '' : ' (Disabled)')\">\n            <x-frontend::page-header.actions>\n                <x-form.button dusk=\"lighthouse-edit-button\"\n                    href=\"{{ route('certificates.edit', ['monitor' => $monitor]) }}\">\n                    @lang('Edit')\n                </x-form.button>\n                <x-form.button class=\"bg-red\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.button>\n            </x-frontend::page-header.actions>\n            \n            <x-frontend::page-header.mobile-actions>\n                <x-form.dropdown-button href=\"{{ route('certificates.edit', ['monitor' => $monitor]) }}\">\n                    @lang('Edit')\n                </x-form.dropdown-button>\n                <x-form.dropdown-button class=\"!text-red hover:!text-red-light\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.dropdown-button>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    <livewire:certificate-monitor-dashboard :monitorId=\"$monitor->id\" />\n\n    <div class=\"my-8\">\n        <h2 class=\"text-xl font-bold leading-7 sm:truncate sm:text-2xl sm:tracking-tight text-neutral-100 mb-2\">\n            {{ __('History') }}</h2>\n        <p class=\"text-sm text-neutral-400 mb-4\">\n            @lang('View the history of this certificate monitor.')\n        </p>\n        <livewire:certificate-monitor-history-table :monitorId=\"$monitor->id\" />\n    </div>\n\n    <!-- Delete Confirmation Modal -->\n    <div x-data=\"{ showDeleteModal: false }\" @open-delete-modal.window=\"showDeleteModal = true\">\n        <x-frontend::modal show=\"showDeleteModal\">\n            <x-frontend::modal.header icon=\"phosphor-trash\" iconColor=\"red\" show=\"showDeleteModal\">\n                @lang('Delete Certificate Monitor')\n            </x-frontend::modal.header>\n\n            <x-frontend::modal.body>\n                <div class=\"space-y-4\">\n                    <p class=\"text-base-100\">\n                        @lang('Are you sure you want to delete this certificate monitor?')\n                    </p>\n                    <div class=\"bg-base-850 border border-base-700 rounded-lg p-4\">\n                        <div class=\"flex items-start gap-3\">\n                            <div class=\"flex-shrink-0\">\n                                @svg('phosphor-warning-circle', 'w-5 h-5 text-orange mt-0.5')\n                            </div>\n                            <div class=\"flex-1\">\n                                <p class=\"text-sm text-base-300\">\n                                    <span class=\"font-semibold text-base-100\">{{ $monitor->domain }}</span>\n                                </p>\n                                <p class=\"text-sm text-base-400 mt-1\">\n                                    @lang('This action cannot be undone. All certificate history for this monitor will be permanently deleted.')\n                                </p>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </x-frontend::modal.body>\n\n            <x-frontend::modal.footer>\n                <x-form.button type=\"button\" @click=\"showDeleteModal = false\">\n                    @lang('Cancel')\n                </x-form.button>\n                <form action=\"{{ route('certificates.delete', ['monitor' => $monitor]) }}\" method=\"POST\" class=\"inline\">\n                    @csrf\n                    @method('DELETE')\n                    <x-form.button class=\"bg-red\" type=\"submit\">\n                        @lang('Delete Monitor')\n                    </x-form.button>\n                </form>\n            </x-frontend::modal.footer>\n        </x-frontend::modal>\n    </div>\n\n</x-app-layout>\n"
  },
  {
    "path": "packages/certificates/routes/web.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\Certificates\\Http\\Controllers\\CertificateMonitorController;\nuse Vigilant\\Certificates\\Livewire\\CertificateMonitorForm;\n\nRoute::prefix('certificates')\n    ->group(function () {\n        Route::get('/', [CertificateMonitorController::class, 'list'])->name('certificates');\n        Route::get('/create', CertificateMonitorForm::class)->name('certificates.create');\n        Route::get('/{monitor}', [CertificateMonitorController::class, 'index'])->name('certificates.index')->can('view,monitor');\n        Route::get('/edit/{monitor}', CertificateMonitorForm::class)->name('certificates.edit');\n        Route::delete('/{monitor}', [CertificateMonitorController::class, 'delete'])->name('certificates.delete')->can('delete,monitor');\n    });\n"
  },
  {
    "path": "packages/certificates/src/Actions/CheckCertificate.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Actions;\n\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Certificates\\Notifications\\CertificateChangedNotification;\nuse Vigilant\\Certificates\\Notifications\\CertificateExpiredNotification;\nuse Vigilant\\Certificates\\Notifications\\CertificateExpiresInDaysNotification;\nuse Vigilant\\Certificates\\Notifications\\UnableToResolveCertificateNotification;\n\nclass CheckCertificate\n{\n    public function check(CertificateMonitor $monitor): void\n    {\n        $context = stream_context_create([\n            'ssl' => [\n                'capture_peer_cert' => true,\n                'verify_peer' => false,\n                'verify_peer_name' => false,\n            ],\n        ]);\n\n        $client = @stream_socket_client(\n            \"ssl://{$monitor->domain}:{$monitor->port}\",\n            $errno,\n            $errstr,\n            30,\n            STREAM_CLIENT_CONNECT,\n            $context\n        );\n\n        if ($client === false) {\n            UnableToResolveCertificateNotification::notify($monitor, $errstr);\n\n            return;\n        }\n\n        $metadata = stream_get_meta_data($client);\n\n        $contParams = stream_context_get_params($client);\n        $certificate = openssl_x509_parse($contParams['options']['ssl']['peer_certificate']);\n\n        if ($certificate === false) {\n            UnableToResolveCertificateNotification::notify($monitor, 'Unable to parse certificate');\n\n            return;\n        }\n\n        $fingerprint = openssl_x509_fingerprint($contParams['options']['ssl']['peer_certificate'], 'sha256');\n\n        $validTo = Carbon::createFromTimestampUTC(data_get($certificate, 'validTo_time_t'));\n\n        if ($validTo->isAfter(now()->addDays(30))) {\n            $nextCheck = now()->addDays(30);\n        } elseif ($validTo->isAfter(now()->addDays(7))) {\n            $nextCheck = now()->addDays(7);\n        } else {\n            $nextCheck = $validTo->subDay();\n\n            if ($nextCheck->isPast()) {\n                $nextCheck = now()->addHours(3);\n            }\n        }\n\n        if ($validTo->isPast()) {\n            CertificateExpiredNotification::notify($monitor);\n        } else {\n\n        }\n\n        if ($monitor->fingerprint !== $fingerprint) {\n            $history = $monitor->history()->create([\n                'serial_number' => data_get($certificate, 'serialNumber'),\n                'protocol' => data_get($metadata, 'crypto.protocol'),\n                'fingerprint' => $fingerprint,\n                'valid_from' => Carbon::createFromTimestampUTC(data_get($certificate, 'validFrom_time_t')),\n                'valid_to' => $validTo,\n                'data' => array_merge(\n                    $certificate,\n                    [\n                        'metadata' => $metadata,\n                    ]\n                ),\n            ]);\n\n            CertificateChangedNotification::notify($monitor, $history);\n        }\n\n        $monitor->update([\n            'next_check' => $nextCheck,\n            'serial_number' => data_get($certificate, 'serialNumber'),\n            'protocol' => data_get($metadata, 'crypto.protocol'),\n            'fingerprint' => $fingerprint,\n            'valid_from' => Carbon::createFromTimestampUTC(data_get($certificate, 'validFrom_time_t')),\n            'valid_to' => $validTo,\n            'data' => array_merge(\n                $certificate,\n                [\n                    'metadata' => $metadata,\n                ]\n            ),\n        ]);\n\n        if ($validTo->isFuture()) {\n            CertificateExpiresInDaysNotification::notify($monitor);\n        }\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Commands/CheckCertificateCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Certificates\\Jobs\\CheckCertificateJob;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\n\nclass CheckCertificateCommand extends Command\n{\n    protected $signature = 'certificates:check {id}';\n\n    public function handle(): int\n    {\n        /** @var int $id */\n        $id = $this->argument('id');\n\n        $monitor = CertificateMonitor::query()\n            ->withoutGlobalScopes()\n            ->findOrFail($id);\n\n        CheckCertificateJob::dispatch($monitor);\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Commands/CheckCertificatesCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Foundation\\Bus\\PendingDispatch;\nuse Vigilant\\Certificates\\Jobs\\CheckCertificateJob;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\n\nclass CheckCertificatesCommand extends Command\n{\n    protected $signature = 'certificates:check-scheduled';\n\n    public function handle(): int\n    {\n        CertificateMonitor::query()\n            ->withoutGlobalScopes()\n            ->where('enabled', '=', true)\n            ->where(function (Builder $query): void {\n                $query->where('next_check', '<=', now())\n                    ->orWhereNull('next_check');\n            })\n            ->get()\n            ->each(fn (CertificateMonitor $monitor): PendingDispatch => CheckCertificateJob::dispatch($monitor));\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Http/Controllers/CertificateMonitorController.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Http\\Controllers;\n\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\View\\View;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\n\nclass CertificateMonitorController extends Controller\n{\n    use DisplaysAlerts;\n\n    public function list(): View\n    {\n        /** @var view-string $view */\n        $view = 'certificates::index';\n        $hasMonitors = CertificateMonitor::query()->exists();\n\n        return view($view, [\n            'hasMonitors' => $hasMonitors,\n        ]);\n    }\n\n    public function index(CertificateMonitor $monitor): mixed\n    {\n        /** @var view-string $view */\n        $view = 'certificates::monitor.index';\n\n        return view($view, [\n            'monitor' => $monitor,\n        ]);\n    }\n\n    public function delete(CertificateMonitor $monitor): RedirectResponse\n    {\n        $monitor->delete();\n\n        $this->alert(\n            __('Deleted'),\n            __('Certificate monitor was successfully deleted'),\n            AlertType::Success\n        );\n\n        return response()->redirectToRoute('certificates');\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Jobs/CheckCertificateJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Certificates\\Actions\\CheckCertificate;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Core\\Services\\TeamService;\n\nclass CheckCertificateJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(\n        public CertificateMonitor $monitor\n    ) {\n        $this->onQueue(config()->string('certificates.queue'));\n    }\n\n    public function handle(CheckCertificate $certificate, TeamService $teamService): void\n    {\n        $teamService->setTeamById($this->monitor->team_id);\n        $certificate->check($this->monitor);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->monitor->id;\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Livewire/CertificateMonitorForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Livewire;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Frontend\\Traits\\CanBeInline;\n\nclass CertificateMonitorForm extends Component\n{\n    use CanBeInline;\n    use DisplaysAlerts;\n\n    public Forms\\CertificateMonitorForm $form;\n\n    #[Locked]\n    public CertificateMonitor $certificateMonitor;\n\n    public function mount(?CertificateMonitor $monitor): void\n    {\n        if ($monitor !== null) {\n            if ($monitor->exists) {\n                $this->authorize('update', $monitor);\n            } else {\n                $this->authorize('create', $monitor);\n            }\n\n            $this->form->fill($monitor->toArray());\n            $this->certificateMonitor = $monitor;\n        }\n    }\n\n    #[On('save')]\n    public function save(): void\n    {\n        $this->validate();\n\n        if ($this->certificateMonitor->exists) {\n            $this->authorize('update', $this->certificateMonitor);\n\n            $this->certificateMonitor->update($this->form->all());\n        } else {\n            $this->authorize('create', $this->certificateMonitor);\n\n            $this->certificateMonitor = CertificateMonitor::query()->create(\n                $this->form->all()\n            );\n        }\n\n        if (! $this->inline) {\n            $this->alert(\n                __('Saved'),\n                __('Certificate monitor was successfully :action',\n                    ['action' => $this->certificateMonitor->wasRecentlyCreated ? 'created' : 'saved']),\n                AlertType::Success\n            );\n            $this->redirectRoute('certificates.index', ['monitor' => $this->certificateMonitor]);\n        }\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'certificates::livewire.certificate-monitor-form';\n\n        return view($view, [\n            'updating' => $this->certificateMonitor->exists,\n        ]);\n    }\n\n}\n"
  },
  {
    "path": "packages/certificates/src/Livewire/Forms/CertificateMonitorForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Form;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Core\\Validation\\CanEnableRule;\nuse Vigilant\\Frontend\\Validation\\CleanDomainValidator;\nuse Vigilant\\Frontend\\Validation\\Fqdn;\n\nclass CertificateMonitorForm extends Form\n{\n    #[Locked]\n    public ?int $site_id;\n\n    public string $domain = '';\n\n    #[Validate('required|integer|min:1|max:65535')]\n    public int $port = 443;\n\n    public bool $enabled = true;\n\n    public function getRules(): array\n    {\n        return array_merge(parent::getRules(),\n            [\n                'enabled' => ['boolean', new CanEnableRule(CertificateMonitor::class)],\n                'domain' => ['required', 'string', 'max:255', new CleanDomainValidator, new Fqdn],\n            ]\n        );\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Livewire/Monitor/Dashboard.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Livewire\\Monitor;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\n\nclass Dashboard extends Component\n{\n    #[Locked]\n    public int $monitorId;\n\n    public function mount(int $monitorId): void\n    {\n        $this->monitorId = $monitorId;\n    }\n\n    public function render(): mixed\n    {\n        $monitor = CertificateMonitor::query()->findOrFail($this->monitorId);\n\n        /** @var view-string $view */\n        $view = 'certificates::livewire.monitor.dashboard';\n\n        return view($view, [\n            'monitor' => $monitor,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Livewire/Tables/CertificateMonitorHistoryTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Livewire\\Attributes\\Locked;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse Vigilant\\Certificates\\Models\\CertificateMonitorHistory;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\DateColumn;\n\nclass CertificateMonitorHistoryTable extends BaseTable\n{\n    protected string $model = CertificateMonitorHistory::class;\n\n    #[Locked]\n    public int $monitorId;\n\n    public function mount(int $monitorId): void\n    {\n        $this->monitorId = $monitorId;\n    }\n\n    protected function columns(): array\n    {\n        return [\n            Column::make(__('Domain'), 'certificateMonitor.domain')\n                ->sortable()\n                ->searchable(),\n\n            DateColumn::make(__('Changed At'), 'created_at')\n                ->sortable(),\n\n            Column::make(__('Issuer'))\n                ->displayUsing(function (CertificateMonitorHistory $monitor): string {\n                    return data_get($monitor->data ?? [], 'issuer.CN', __('Unknown'));\n                }),\n\n            DateColumn::make(__('Valid From'), 'valid_from')\n                ->sortable()\n                ->searchable(),\n\n            DateColumn::make(__('Valid To'), 'valid_to')\n                ->sortable()\n                ->searchable(),\n\n            Column::make(__('Protocol'), 'protocol')\n                ->hide()\n                ->sortable()\n                ->searchable(),\n        ];\n    }\n\n    protected function query(): Builder\n    {\n        return parent::query()\n            ->where('certificate_monitor_id', '=', $this->monitorId);\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Livewire/Tables/CertificateMonitorsTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Enumerable;\nuse Illuminate\\Support\\Facades\\Gate;\nuse RamonRietdijk\\LivewireTables\\Actions\\Action;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse RamonRietdijk\\LivewireTables\\Filters\\SelectFilter;\nuse Vigilant\\Certificates\\Jobs\\CheckCertificateJob;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\DateColumn;\nuse Vigilant\\Frontend\\Integrations\\Table\\Enums\\Status;\nuse Vigilant\\Frontend\\Integrations\\Table\\StatusColumn;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass CertificateMonitorsTable extends BaseTable\n{\n    protected string $model = CertificateMonitor::class;\n\n    protected function columns(): array\n    {\n        return [\n            Column::make(__('Domain'), 'domain')\n                ->sortable()\n                ->searchable(),\n\n            StatusColumn::make(__('Expires In'), 'valid_to', 'expires_in')\n                ->sortable()\n                ->text(function (CertificateMonitor $monitor): string {\n                    if (! $monitor->enabled) {\n                        return __('Disabled');\n                    }\n\n                    if ($monitor->valid_to === null) {\n                        return __('Unknown');\n                    }\n\n                    if ($monitor->valid_to->isPast()) {\n                        return __('Expired');\n                    }\n\n                    return $monitor->valid_to->longRelativeDiffForHumans();\n\n                })\n                ->status(function (CertificateMonitor $monitor): Status {\n                    if (! $monitor->enabled) {\n                        return Status::Danger;\n                    }\n\n                    if ($monitor->valid_to === null) {\n                        return Status::Danger;\n                    }\n\n                    if ($monitor->valid_to->isPast()) {\n                        return Status::Danger;\n                    }\n\n                    if ($monitor->valid_to->greaterThan(now()->addWeek())) {\n                        return Status::Success;\n                    }\n\n                    return Status::Warning;\n                }),\n\n            Column::make(__('Issuer'))\n                ->displayUsing(function (CertificateMonitor $monitor): string {\n                    return data_get($monitor->data ?? [], 'issuer.CN', __('Unknown'));\n                }),\n\n            DateColumn::make(__('Valid From'), 'valid_from')\n                ->sortable()\n                ->searchable(),\n\n            DateColumn::make(__('Valid To'), 'valid_to')\n                ->sortable()\n                ->searchable(),\n\n            Column::make(__('Protocol'), 'protocol')\n                ->hide()\n                ->sortable()\n                ->searchable(),\n        ];\n    }\n\n    protected function filters(): array\n    {\n        return [\n            SelectFilter::make(__('Site'), 'site_id')\n                ->options(\n                    Site::query()\n                        ->orderBy('url')\n                        ->pluck('url', 'id')\n                        ->toArray()\n                ),\n        ];\n    }\n\n    protected function actions(): array\n    {\n        return [\n            Action::make(__('Check Now'), function (Enumerable $models): void {\n                $models->each(fn (CertificateMonitor $monitor) => CheckCertificateJob::dispatch($monitor));\n            }, 'run'),\n\n            Action::make(__('Enable'), function (Enumerable $models): void {\n                foreach ($models as $model) {\n                    if (! Gate::allows('create', $model)) {\n                        break;\n                    }\n\n                    $model->update(['enabled' => true]);\n                }\n            }, 'enable'),\n\n            Action::make(__('Disable'), function (Enumerable $models): void {\n                $models->each(fn (CertificateMonitor $monitor) => $monitor->update(['enabled' => false]));\n            }, 'disable'),\n\n            Action::make(__('Delete'), function (Enumerable $models): void {\n                $models->each(fn (CertificateMonitor $monitor): ?bool => $monitor->delete());\n            }, 'delete'),\n        ];\n    }\n\n    protected function link(Model $model): ?string\n    {\n        return route('certificates.index', ['monitor' => $model]);\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Models/CertificateMonitor.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Observers\\TeamObserver;\n\n/**\n * @property int $id\n * @property int $site_id\n * @property int $team_id\n * @property bool $enabled\n * @property ?Carbon $next_check\n * @property string $domain\n * @property int $port\n * @property ?string $serial_number\n * @property ?string $fingerprint\n * @property ?string $protocol\n * @property ?Carbon $valid_from\n * @property ?Carbon $valid_to\n * @property ?array $data\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property Site $site\n * @property Team $team\n * @property Collection<int, CertificateMonitorHistory> $history\n */\n#[ObservedBy(TeamObserver::class)]\n#[ScopedBy(TeamScope::class)]\nclass CertificateMonitor extends Model\n{\n    protected $guarded = [];\n\n    protected $casts = [\n        'enabled' => 'boolean',\n        'next_check' => 'datetime',\n        'valid_from' => 'datetime',\n        'valid_to' => 'datetime',\n        'data' => 'array',\n    ];\n\n    public function history(): HasMany\n    {\n        return $this->hasMany(CertificateMonitorHistory::class);\n    }\n\n    public function site(): BelongsTo\n    {\n        return $this->belongsTo(Site::class);\n    }\n\n    public function team(): BelongsTo\n    {\n        return $this->belongsTo(Team::class);\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Models/CertificateMonitorHistory.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Prunable;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Concerns\\HasDataRetention;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Observers\\TeamObserver;\n\n/**\n * @property int $id\n * @property int $certificate_monitor_id\n * @property int $team_id\n * @property ?string $serial_number\n * @property ?string $protocol\n * @property ?Carbon $valid_from\n * @property ?Carbon $valid_to\n * @property ?array $data\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property CertificateMonitor $certificateMonitor\n * @property Team $team\n */\n#[ObservedBy(TeamObserver::class)]\n#[ScopedBy(TeamScope::class)]\nclass CertificateMonitorHistory extends Model\n{\n    use HasDataRetention;\n    use Prunable;\n\n    protected $guarded = [];\n\n    protected $casts = [\n        'valid_from' => 'datetime',\n        'valid_to' => 'datetime',\n        'data' => 'array',\n    ];\n\n    public function certificateMonitor(): BelongsTo\n    {\n        return $this->belongsTo(CertificateMonitor::class);\n    }\n\n    public function team(): BelongsTo\n    {\n        return $this->belongsTo(Team::class);\n    }\n\n    public function prunable(): Builder\n    {\n        return static::withoutGlobalScopes()->where('created_at', '<=', $this->retentionPeriod());\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Notifications/CertificateChangedNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Notifications;\n\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Certificates\\Models\\CertificateMonitorHistory;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass CertificateChangedNotification extends Notification implements HasSite\n{\n    public static string $name = 'Certificate Changed';\n\n    public Level $level = Level::Info;\n\n    public function __construct(public CertificateMonitor $monitor, public CertificateMonitorHistory $old) {}\n\n    public function title(): string\n    {\n        return __('The certificate on :domain has been updated', [\n            'domain' => $this->monitor->domain,\n        ]);\n    }\n\n    public function description(): string\n    {\n        return __('The certificate on :domain has been updated. The new expiration date is :validto, old expiration date was :oldvalidto', [\n            'domain' => $this->monitor->domain,\n            'validto' => $this->monitor->valid_to?->toDateString() ?? '?',\n            'oldvalidto' => $this->old->valid_to?->toDateString() ?? '?',\n        ]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when an SSL certificate is replaced or renewed.');\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->monitor->id;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->monitor->site;\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Notifications/CertificateExpiredNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Notifications;\n\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass CertificateExpiredNotification extends Notification implements HasSite\n{\n    public static string $name = 'Certificate Expired';\n\n    public Level $level = Level::Critical;\n\n    public static ?int $defaultCooldown = 60 * 24 * 7;\n\n    public function __construct(public CertificateMonitor $monitor) {}\n\n    public function title(): string\n    {\n        return __('The certificate on :domain expired on :validTo', [\n            'domain' => $this->monitor->domain,\n            'validto' => $this->monitor->valid_to?->toDateString() ?? '?',\n        ]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when an SSL certificate has expired.');\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->monitor->id;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->monitor->site;\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Notifications/CertificateExpiresInDaysNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Notifications;\n\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Certificates\\Notifications\\Conditions\\DaysCondition;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass CertificateExpiresInDaysNotification extends Notification implements HasSite\n{\n    public static string $name = 'Certificate Expires In Days';\n\n    public Level $level = Level::Warning;\n\n    public static ?int $defaultCooldown = 60 * 24;\n\n    public static array $defaultConditions = [\n        'type' => 'group',\n        'children' => [\n            [\n                'type' => 'condition',\n                'condition' => DaysCondition::class,\n                'value' => 14,\n            ],\n        ],\n    ];\n\n    public function __construct(public CertificateMonitor $monitor) {}\n\n    public function title(): string\n    {\n        return __('The certificate on :domain will expire in :difference', [\n            'domain' => $this->monitor->domain,\n            'difference' => $this->monitor->valid_to?->shortAbsoluteDiffForHumans() ?? '?',\n        ]);\n    }\n\n    public function description(): string\n    {\n        return __('The certificate on :domain will expire on :date which is :difference', [\n            'domain' => $this->monitor->domain,\n            'validto' => $this->monitor->valid_to?->shortAbsoluteDiffForHumans() ?? '?',\n            'date' => $this->monitor->valid_to?->toDateString() ?? '?',\n        ]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when an SSL certificate is approaching expiration.');\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->monitor->id;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->monitor->site;\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Notifications/Conditions/DaysCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Notifications\\Conditions;\n\nuse Vigilant\\Certificates\\Notifications\\CertificateExpiredNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Enums\\ConditionType;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass DaysCondition extends Condition\n{\n    public static string $name = 'Expires within days';\n\n    public ConditionType $type = ConditionType::Number;\n\n    public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool\n    {\n        /** @var CertificateExpiredNotification $notification */\n        $value = (int) $value;\n\n        $validTo = $notification->monitor->valid_to;\n\n        if ($validTo === null) {\n            return false;\n        }\n\n        return $validTo->diffInDays(now()) <= $value\n            && $validTo->diffInDays(now()) > 0;\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/Notifications/UnableToResolveCertificateNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Notifications;\n\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass UnableToResolveCertificateNotification extends Notification implements HasSite\n{\n    public static string $name = 'Unable to resolve certificate';\n\n    public Level $level = Level::Info;\n\n    public static ?int $defaultCooldown = 60 * 24;\n\n    public function __construct(public CertificateMonitor $monitor, public string $error) {}\n\n    public function title(): string\n    {\n        return __('Unable to resolve certificate for :domain', [\n            'domain' => $this->monitor->domain,\n        ]);\n    }\n\n    public function description(): string\n    {\n        return __('Error: :error', [\n            'error' => $this->error,\n        ]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when an SSL certificate cannot be retrieved or validated.');\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->monitor->id;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->monitor->site;\n    }\n}\n"
  },
  {
    "path": "packages/certificates/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates;\n\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\nuse Livewire\\Livewire;\nuse Vigilant\\Certificates\\Commands\\CheckCertificateCommand;\nuse Vigilant\\Certificates\\Commands\\CheckCertificatesCommand;\nuse Vigilant\\Certificates\\Livewire\\CertificateMonitorForm;\nuse Vigilant\\Certificates\\Livewire\\Monitor\\Dashboard;\nuse Vigilant\\Certificates\\Livewire\\Tables\\CertificateMonitorHistoryTable;\nuse Vigilant\\Certificates\\Livewire\\Tables\\CertificateMonitorsTable;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Certificates\\Notifications\\CertificateExpiredNotification;\nuse Vigilant\\Certificates\\Notifications\\CertificateExpiresInDaysNotification;\nuse Vigilant\\Certificates\\Notifications\\Conditions\\DaysCondition;\nuse Vigilant\\Core\\Facades\\Navigation;\nuse Vigilant\\Core\\Policies\\AllowAllPolicy;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Sites\\Conditions\\SiteCondition;\nuse Vigilant\\Users\\Models\\User;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function register(): void\n    {\n        $this\n            ->registerConfig();\n    }\n\n    protected function registerConfig(): static\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/certificates.php', 'certificates');\n\n        return $this;\n    }\n\n    public function boot(): void\n    {\n        $this\n            ->bootRoutes()\n            ->bootConfig()\n            ->bootMigrations()\n            ->bootCommands()\n            ->bootViews()\n            ->bootLivewire()\n            ->bootNavigation()\n            ->bootNotifications()\n            ->bootGates()\n            ->bootPolicies();\n    }\n\n    protected function bootRoutes(): static\n    {\n        if (! $this->app->routesAreCached()) {\n            Route::middleware(['web', 'auth'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/web.php'));\n        }\n\n        return $this;\n    }\n\n    protected function bootConfig(): static\n    {\n        $this->publishes([\n            __DIR__.'/../config/certificates.php' => config_path('certificates.php'),\n        ], 'config');\n\n        return $this;\n    }\n\n    protected function bootMigrations(): static\n    {\n        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');\n\n        return $this;\n    }\n\n    protected function bootCommands(): static\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n                CheckCertificateCommand::class,\n                CheckCertificatesCommand::class,\n\n            ]);\n        }\n\n        return $this;\n    }\n\n    protected function bootViews(): static\n    {\n        $this->loadViewsFrom(__DIR__.'/../resources/views', 'certificates');\n\n        return $this;\n    }\n\n    protected function bootLivewire(): static\n    {\n        Livewire::component('certificate-monitor-table', CertificateMonitorsTable::class);\n        Livewire::component('certificate-monitor-history-table', CertificateMonitorHistoryTable::class);\n        Livewire::component('certificate-monitor-form', CertificateMonitorForm::class);\n        Livewire::component('certificate-monitor-dashboard', Dashboard::class);\n\n        return $this;\n    }\n\n    protected function bootNavigation(): static\n    {\n        Navigation::path(__DIR__.'/../resources/navigation.php');\n\n        return $this;\n    }\n\n    protected function bootNotifications(): static\n    {\n        NotificationRegistry::registerNotification([\n            CertificateExpiresInDaysNotification::class,\n            CertificateExpiredNotification::class,\n        ]);\n\n        NotificationRegistry::registerCondition(CertificateExpiresInDaysNotification::class, [\n            SiteCondition::class,\n            DaysCondition::class,\n        ]);\n\n        NotificationRegistry::registerCondition(CertificateExpiredNotification::class, [\n            SiteCondition::class,\n        ]);\n\n        return $this;\n    }\n\n    protected function bootGates(): static\n    {\n        if (ce()) {\n            Gate::define('use-certificates', function (User $user): bool {\n                return ce();\n            });\n        }\n\n        return $this;\n    }\n\n    protected function bootPolicies(): static\n    {\n        if (ce()) {\n            Gate::policy(CertificateMonitor::class, AllowAllPolicy::class);\n        }\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/certificates/testbench.yaml",
    "content": "providers:\n  - Vigilant\\Certificates\\ServiceProvider\n"
  },
  {
    "path": "packages/certificates/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\Certificates\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Livewire\\LivewireServiceProvider;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Certificates\\ServiceProvider;\nuse Vigilant\\Core\\Services\\TeamService;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        TeamService::fake();\n    }\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n            \\Vigilant\\Users\\ServiceProvider::class,\n            LivewireServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/core/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/core/composer.json",
    "content": "{\n    \"name\": \"vigilant/core\",\n    \"description\": \"Vigilant Core\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"laravel/framework\": \"^12.0\",\n        \"vigilant/certificates\": \"@dev\",\n        \"vigilant/crawler\": \"@dev\",\n        \"vigilant/cve\": \"@dev\",\n        \"vigilant/dns\": \"@dev\",\n        \"vigilant/frontend\": \"@dev\",\n        \"vigilant/lighthouse\": \"@dev\",\n        \"vigilant/notifications\": \"@dev\",\n        \"vigilant/onboarding\": \"@dev\",\n        \"vigilant/settings\": \"@dev\",\n        \"vigilant/sites\": \"@dev\",\n        \"vigilant/uptime\": \"@dev\",\n        \"vigilant/users\": \"@dev\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Core\\\\\": \"src\"\n        },\n        \"files\": [\n            \"src/helpers.php\"\n        ]\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Core\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\Core\\\\ServiceProvider\"\n            ],\n            \"aliases\": {\n                \"Navigation\": \"Vigilant\\\\Core\\\\Facades\\\\Navigation\"\n            }\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/core/config/core.php",
    "content": "<?php\n\nuse Vigilant\\Certificates\\Models\\CertificateMonitorHistory;\nuse Vigilant\\Dns\\Models\\DnsMonitorHistory;\nuse Vigilant\\Healthchecks\\Models\\Metric;\nuse Vigilant\\Healthchecks\\Models\\Result as HealthcheckResult;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Notifications\\Models\\History;\nuse Vigilant\\Uptime\\Models\\Downtime;\nuse Vigilant\\Uptime\\Models\\Result;\n\nreturn [\n    'edition' => env('EDITION', 'ce'),\n\n    'user_agent' => 'Vigilant Bot',\n\n    'data_retention' => [\n        DnsMonitorHistory::class => env('DATA_RETENTION_DNS_MONITOR_HISTORY', 180),\n        Downtime::class => env('DATA_RETENTION_DOWNTIME', 730),\n        Result::class => env('DATA_RETENTION_UPTIME_RESULT', 180),\n        LighthouseResult::class => env('DATA_RETENTION_LIGHTHOUSE', 180),\n        History::class => env('DATA_RETENTION_NOTIFICATION_HISTORY', 90),\n        CertificateMonitorHistory::class => env('DATA_RETENTION_CERTIFICATE_MONITOR_HISTORY', 180),\n        HealthcheckResult::class => env('DATA_RETENTION_HEALTHCHECK_RESULT', 180),\n        Metric::class => env('DATA_RETENTION_HEALTHCHECK_METRIC', 180),\n    ],\n];\n"
  },
  {
    "path": "packages/core/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n    ignoreErrors:\n        - identifier: missingType.iterableValue\n        - identifier: missingType.generics\n        - identifier: trait.unused\n"
  },
  {
    "path": "packages/core/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/core/resources/navigation.php",
    "content": "<?php\n\nuse Vigilant\\Core\\Facades\\Navigation;\n\nNavigation::add(null, 'Health')\n    ->code('health')\n    ->icon('phosphor-heart-half-duotone')\n    ->sort(200);\n\nNavigation::add(null, 'Performance')\n    ->code('performance')\n    ->icon('carbon-chart-line-smooth')\n    ->sort(300);\n\nNavigation::add(null, 'Infrastructure')\n    ->code('infrastructure')\n    ->icon('carbon-cloud-monitoring')\n    ->sort(400);\n"
  },
  {
    "path": "packages/core/routes/web.php",
    "content": "<?php\n"
  },
  {
    "path": "packages/core/src/Actions/ResolveDataRetention.php",
    "content": "<?php\n\nnamespace Vigilant\\Core\\Actions;\n\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Contracts\\ResolvesDataRetention;\n\nclass ResolveDataRetention implements ResolvesDataRetention\n{\n    public function resolve(string $class): Carbon\n    {\n        $days = config('core.data_retention.'.$class, 365);\n\n        return now()->subDays($days);\n    }\n}\n"
  },
  {
    "path": "packages/core/src/Concerns/HasDataRetention.php",
    "content": "<?php\n\nnamespace Vigilant\\Core\\Concerns;\n\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Contracts\\ResolvesDataRetention;\n\ntrait HasDataRetention\n{\n    protected function retentionPeriod(): Carbon\n    {\n        $resolver = app(ResolvesDataRetention::class);\n\n        return $resolver->resolve(static::class);\n    }\n}\n"
  },
  {
    "path": "packages/core/src/Contracts/ResolvesDataRetention.php",
    "content": "<?php\n\nnamespace Vigilant\\Core\\Contracts;\n\nuse Illuminate\\Support\\Carbon;\n\ninterface ResolvesDataRetention\n{\n    public function resolve(string $class): Carbon;\n}\n"
  },
  {
    "path": "packages/core/src/Data/Data.php",
    "content": "<?php\n\nnamespace Vigilant\\Core\\Data;\n\nuse ArrayAccess;\nuse Illuminate\\Contracts\\Support\\Arrayable;\nuse Illuminate\\Support\\Facades\\Validator;\n\nabstract class Data implements Arrayable, ArrayAccess\n{\n    public array $rules = [];\n\n    final public function __construct(\n        public array $data\n    ) {\n        $this->validate($data);\n    }\n\n    public function offsetExists(mixed $offset): bool\n    {\n        return array_key_exists($offset, $this->data);\n    }\n\n    public function offsetGet(mixed $offset): mixed\n    {\n        return $this->data[$offset] ?? null;\n    }\n\n    public function offsetSet(mixed $offset, mixed $value): void\n    {\n        $this->data[$offset] = $value;\n    }\n\n    public function offsetUnset(mixed $offset): void\n    {\n        unset($this->data[$offset]);\n    }\n\n    public function validate(array $data): void\n    {\n        Validator::make($data, $this->rules)->validate();\n    }\n\n    public static function of(array $data): static\n    {\n        return new static($data);\n    }\n\n    public function toArray(): array\n    {\n        return $this->data;\n    }\n}\n"
  },
  {
    "path": "packages/core/src/Facades/Navigation.php",
    "content": "<?php\n\nnamespace Vigilant\\Core\\Facades;\n\nuse Illuminate\\Support\\Facades\\Facade;\nuse Vigilant\\Core\\Navigation\\NavigationItem;\n\n/**\n * @method static Navigation path(string $path)\n * @method static NavigationItem add(string $url, string $name)\n * @method static array items()\n */\nclass Navigation extends Facade\n{\n    protected static function getFacadeAccessor(): string\n    {\n        return \\Vigilant\\Core\\Navigation\\Navigation::class;\n    }\n}\n"
  },
  {
    "path": "packages/core/src/Http/Middleware/TeamMiddleware.php",
    "content": "<?php\n\nnamespace Vigilant\\Core\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Http\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Vigilant\\Core\\Services\\TeamService;\n\nclass TeamMiddleware\n{\n    public function __construct(protected TeamService $teamService) {}\n\n    /**\n     * @param  \\Closure(\\Illuminate\\Http\\Request): (\\Symfony\\Component\\HttpFoundation\\Response)  $next\n     */\n    public function handle(Request $request, Closure $next): Response\n    {\n        $this->teamService->fromAuth();\n\n        return $next($request);\n    }\n}\n"
  },
  {
    "path": "packages/core/src/Navigation/Navigation.php",
    "content": "<?php\n\nnamespace Vigilant\\Core\\Navigation;\n\nclass Navigation\n{\n    protected array $paths = [];\n\n    protected bool $loaded = false;\n\n    protected array $items = [];\n\n    public function path(string $path): static\n    {\n        $this->paths[] = $path;\n\n        return $this;\n    }\n\n    public function add(\n        ?string $url,\n        string $name,\n    ): NavigationItem {\n        $item = new NavigationItem($name, $url);\n\n        $this->items[] = $item;\n\n        return $item;\n    }\n\n    public function items(): array\n    {\n        if (! $this->loaded) {\n            foreach ($this->paths as $path) {\n                require $path;\n            }\n\n            $this->loaded = true;\n        }\n\n        return collect($this->items)\n            ->map(function (NavigationItem $item) {\n\n                if ($item->parent !== null) {\n                    return null;\n                }\n\n                $children = collect($this->items)\n                    ->filter(fn (NavigationItem $child) => $child->parent === $item->code)\n                    ->sortBy('sort')\n                    ->toArray();\n\n                $item->children = $children;\n\n                return $item;\n            })\n            ->whereNotNull()\n            ->sortBy('sort')\n            ->toArray();\n    }\n}\n"
  },
  {
    "path": "packages/core/src/Navigation/NavigationItem.php",
    "content": "<?php\n\nnamespace Vigilant\\Core\\Navigation;\n\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Route;\n\nclass NavigationItem\n{\n    public array $children = [];\n\n    public function __construct(\n        public string $name,\n        public ?string $url,\n        public ?string $icon = null,\n        public int $sort = 0,\n        public ?array $routeIs = null,\n        public ?string $gate = null,\n        public ?string $code = null,\n        public ?string $parent = null\n    ) {\n        if ($code === null) {\n            $this->code = str($name)\n                ->slug()\n                ->replace('-', '_')\n                ->toString();\n        }\n    }\n\n    public function active(): bool\n    {\n        if ($this->routeIs !== null) {\n            return Route::is($this->routeIs);\n        }\n\n        return request()->url() === $this->url;\n    }\n\n    public function shouldRender(): bool\n    {\n        if ($this->gate === null) {\n            return true;\n        }\n\n        return Gate::check($this->gate);\n    }\n\n    public function name(string $name): static\n    {\n        $this->name = $name;\n\n        return $this;\n    }\n\n    public function routeIs(string ...$routeIs): static\n    {\n        $this->routeIs = $routeIs;\n\n        return $this;\n    }\n\n    public function url(string $url): static\n    {\n        $this->url = $url;\n\n        return $this;\n    }\n\n    public function icon(string $icon): static\n    {\n        $this->icon = $icon;\n\n        return $this;\n    }\n\n    public function gate(string $gate): static\n    {\n        $this->gate = $gate;\n\n        return $this;\n    }\n\n    public function sort(int $sort): static\n    {\n        $this->sort = $sort;\n\n        return $this;\n    }\n\n    public function code(string $code): static\n    {\n        $this->code = $code;\n\n        return $this;\n    }\n\n    public function parent(string $parent): static\n    {\n        $this->parent = $parent;\n\n        return $this;\n    }\n\n    public function hasChildren(): bool\n    {\n        return count($this->children) > 0;\n    }\n\n    public function getChildren(): array\n    {\n        return $this->children;\n    }\n}\n"
  },
  {
    "path": "packages/core/src/Policies/AllowAllPolicy.php",
    "content": "<?php\n\nnamespace Vigilant\\Core\\Policies;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Vigilant\\Users\\Models\\User;\n\nclass AllowAllPolicy\n{\n    public function viewAny(User $user): bool\n    {\n        return true;\n    }\n\n    public function view(User $user, Model $model): bool\n    {\n        return true;\n    }\n\n    public function create(User $user): bool\n    {\n        return true;\n    }\n\n    public function update(User $user, Model $model): bool\n    {\n        return true;\n    }\n\n    public function delete(User $user, Model $model): bool\n    {\n        return true;\n    }\n\n    public function restore(User $user, Model $model): bool\n    {\n        return true;\n    }\n\n    public function forceDelete(User $user, Model $model): bool\n    {\n        return true;\n    }\n}\n"
  },
  {
    "path": "packages/core/src/Scopes/TeamScope.php",
    "content": "<?php\n\nnamespace Vigilant\\Core\\Scopes;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Scope;\nuse Vigilant\\Core\\Services\\TeamService;\n\nclass TeamScope implements Scope\n{\n    public function apply(Builder $builder, Model $model): void\n    {\n        /** @var TeamService $teamService */\n        $teamService = app(TeamService::class);\n        $team = $teamService->team();\n\n        $builder->where($model->qualifyColumn('team_id'), '=', $team->id);\n    }\n}\n"
  },
  {
    "path": "packages/core/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\Core;\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\nuse Vigilant\\Core\\Actions\\ResolveDataRetention;\nuse Vigilant\\Core\\Contracts\\ResolvesDataRetention;\nuse Vigilant\\Core\\Facades\\Navigation as NavigationFacade;\nuse Vigilant\\Core\\Navigation\\Navigation;\nuse Vigilant\\Core\\Services\\TeamService;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function register(): void\n    {\n        $this\n            ->registerConfig()\n            ->registerSingletons();\n    }\n\n    protected function registerConfig(): static\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/core.php', 'core');\n\n        return $this;\n    }\n\n    protected function registerSingletons(): static\n    {\n        $this->app->scoped(TeamService::class);\n        $this->app->singleton(Navigation::class);\n\n        return $this;\n    }\n\n    public function boot(): void\n    {\n        $this\n            ->bootActions()\n            ->bootConfig()\n            ->bootMigrations()\n            ->bootCommands()\n            ->bootRoutes()\n            ->bootNavigation();\n    }\n\n    protected function bootActions(): static\n    {\n        if (ce()) {\n            $this->app->singleton(ResolvesDataRetention::class, ResolveDataRetention::class);\n        }\n\n        return $this;\n    }\n\n    protected function bootConfig(): static\n    {\n        $this->publishes([\n            __DIR__.'/../config/core.php' => config_path('core.php'),\n        ], 'config');\n\n        return $this;\n    }\n\n    protected function bootMigrations(): static\n    {\n        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');\n\n        return $this;\n    }\n\n    protected function bootCommands(): static\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n\n            ]);\n        }\n\n        return $this;\n    }\n\n    protected function bootRoutes(): static\n    {\n        if (! $this->app->routesAreCached()) {\n            Route::middleware(['web', 'auth'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/web.php'));\n        }\n\n        return $this;\n    }\n\n    protected function bootNavigation(): static\n    {\n        NavigationFacade::path(__DIR__.'/../resources/navigation.php');\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/core/src/Services/TeamService.php",
    "content": "<?php\n\nnamespace Vigilant\\Core\\Services;\n\nuse Illuminate\\Support\\Facades\\Auth;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Models\\User;\n\nclass TeamService\n{\n    protected ?Team $team = null;\n\n    public function fromAuth(): void\n    {\n        /** @var ?User $user */\n        $user = Auth::user();\n\n        if ($user === null) {\n            return;\n        }\n\n        /** @var User $user */\n        $this->team = $user->currentTeam;\n    }\n\n    public function setTeam(?Team $team): void\n    {\n        $this->team = $team;\n    }\n\n    public function setTeamById(int $teamId): void\n    {\n        /** @var Team $team */\n        $team = Team::query()->findOrFail($teamId);\n\n        $this->setTeam($team);\n    }\n\n    public function team(): Team\n    {\n        if ($this->team === null) {\n            $this->fromAuth();\n        }\n\n        throw_if($this->team === null, 'No team');\n\n        return $this->team;\n    }\n\n    public static function instance(): TeamService\n    {\n        /** @var TeamService $instance */\n        $instance = app(TeamService::class);\n\n        return $instance;\n    }\n\n    public static function fake(): Team\n    {\n        /** @var Team $team */\n        $team = Team::factory()->create();\n\n        static::instance()->setTeam($team);\n\n        return $team;\n    }\n}\n"
  },
  {
    "path": "packages/core/src/Validation/CanEnableRule.php",
    "content": "<?php\n\nnamespace Vigilant\\Core\\Validation;\n\nuse Closure;\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\nuse Vigilant\\Users\\Models\\User;\n\nclass CanEnableRule implements ValidationRule\n{\n    public function __construct(public string $model) {}\n\n    public function validate(string $attribute, mixed $value, Closure $fail): void\n    {\n        if (is_bool($value) && ! $value) {\n            return;\n        }\n\n        /** @var ?User $user */\n        $user = auth()->user();\n\n        if ($user === null) {\n            return;\n        }\n\n        if (! $user->can('create', $this->model)) {\n            $fail('Unable to enable this resource, check your billing plan.');\n        }\n    }\n}\n"
  },
  {
    "path": "packages/core/src/helpers.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Services\\TeamService;\n\nif (! function_exists('teamTimezone')) {\n    function teamTimezone(Carbon $carbon): Carbon\n    {\n        /** @var TeamService $teamService */\n        $teamService = app(TeamService::class);\n\n        return $carbon->timezone($teamService->team()->timezone ?? 'UTC');\n    }\n}\n\nif (! function_exists('ce')) {\n    function ce(): bool\n    {\n        return config('core.edition', 'ce') === 'ce';\n    }\n}\n"
  },
  {
    "path": "packages/core/testbench.yaml",
    "content": "providers:\n  - Vigilant\\Core\\ServiceProvider\n"
  },
  {
    "path": "packages/core/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\Core\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Core\\ServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/crawler/composer.json",
    "content": "{\n    \"name\": \"vigilant/crawler\",\n    \"description\": \"Vigilant Crawler\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"guzzlehttp/guzzle\": \"^7.8\",\n        \"laravel/framework\": \"^12.0\",\n        \"livewire/livewire\": \"^3.4\",\n        \"maatwebsite/excel\": \"^3.1\",\n        \"vigilant/core\": \"@dev\",\n        \"vigilant/sites\": \"@dev\",\n        \"vigilant/users\": \"@dev\",\n        \"vigilant/frontend\": \"@dev\",\n        \"vigilant/notifications\": \"@dev\",\n        \"mtownsend/xml-to-array\": \"^2.0\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Crawler\\\\\": \"src\",\n            \"Vigilant\\\\Crawler\\\\Database\\\\Factories\\\\\": \"database/factories\",\n            \"Vigilant\\\\Users\\\\Database\\\\Factories\\\\\": \"../users/database/factories\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Crawler\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\Crawler\\\\ServiceProvider\"\n            ]\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/crawler/config/crawler.php",
    "content": "<?php\n\nreturn [\n    'queue' => 'crawler',\n\n    'timeout' => env('CRAWLER_TIMEOUT', 5),\n    'connect_timeout' => env('CRAWLER_CONNECT_TIMEOUT', 2),\n\n    'crawls_per_minute' => (int) env('CRAWLER_CRAWLS_PER_MINUTE', 500),\n\n    'platform_blacklists' => [\n        'magento1' => [\n            'label' => 'Magento 1',\n            'patterns' => [\n                '~^https?://[^/]+/index\\.php/admin~i',\n                '~^https?://[^/]+/admin~i',\n                '~^https?://[^/]+/api/~i',\n                '~^https?://[^/]+/cron\\.php~i',\n                '~^https?://[^/]+/index\\.php/customer/account~i',\n                '~^https?://[^/]+/customer/account/login~i',\n                '~^https?://[^/]+/customer/account/create~i',\n                '~^https?://[^/]+/customer/account/logout~i',\n                '~^https?://[^/]+/checkout/~i',\n                '~^https?://[^/]+/cart~i',\n                '~^https?://[^/]+/wishlist/~i',\n                '~^https?://[^/]+/review/product/~i',\n                '~^https?://[^/]+/newsletter/subscriber/~i',\n                '~^https?://[^/]+/contacts/index/post~i',\n                '~^https?://[^/]+/catalogsearch/ajax/~i',\n                '~^https?://[^/]+/sendfriend/~i',\n                '~^https?://[^/]+/catalog/product_compare/~i',\n                '~^https?://[^/]+/tag/~i',\n                '~^https?://[^/]+/rating/~i',\n                '~^https?://[^/]+/poll/~i',\n                '~^https?://[^/]+/paypal/~i',\n            ],\n        ],\n        'magento2' => [\n            'label' => 'Magento 2',\n            'patterns' => [\n                '~^https?://[^/]+/admin~i',\n                '~^https?://[^/]+/rest/~i',\n                '~^https?://[^/]+/graphql~i',\n                '~^https?://[^/]+/soap/~i',\n                '~^https?://[^/]+/cron\\.php~i',\n                '~^https?://[^/]+/index\\.php/customer/account~i',\n                '~^https?://[^/]+/customer/account/login~i',\n                '~^https?://[^/]+/customer/account/create~i',\n                '~^https?://[^/]+/customer/account/logout~i',\n                '~^https?://[^/]+/checkout/~i',\n                '~^https?://[^/]+/onestepcheckout/~i',\n                '~^https?://[^/]+/cart~i',\n                '~^https?://[^/]+/wishlist/~i',\n                '~^https?://[^/]+/review/product/~i',\n                '~^https?://[^/]+/newsletter/subscriber/~i',\n                '~^https?://[^/]+/contact/index/post~i',\n                '~^https?://[^/]+/search/ajax/~i',\n                '~^https?://[^/]+/catalogsearch/ajax/~i',\n                '~^https?://[^/]+/page_cache/~i',\n                '~^https?://[^/]+/static/version~i',\n                '~^https?://[^/]+/media/tmp/~i',\n                '~^https?://[^/]+/pub/media/tmp/~i',\n                '~^https?://[^/]+/sendfriend/~i',\n                '~^https?://[^/]+/catalog/product_compare/~i',\n            ],\n        ],\n        'wordpress' => [\n            'label' => 'WordPress',\n            'patterns' => [\n                '~^https?://[^/]+/wp-admin~i',\n                '~^https?://[^/]+/wp-login\\.php~i',\n                '~^https?://[^/]+/wp-cron\\.php~i',\n                '~^https?://[^/]+/wp-json/~i',\n                '~^https?://[^/]+/xmlrpc\\.php~i',\n                '~^https?://[^/]+/\\?feed=~i',\n                '~^https?://[^/]+/feed/~i',\n                '~^https?://[^/]+/comments/feed/~i',\n                '~[?&]replytocom=~i',\n                '~[?&]preview=true~i',\n                '~^https?://[^/]+/\\?p=\\d+&preview=true~i',\n                '~^https?://[^/]+/wp-content/uploads/~i',\n                '~^https?://[^/]+/\\?add-to-cart=~i',\n                '~^https?://[^/]+/cart/~i',\n                '~^https?://[^/]+/checkout/~i',\n                '~^https?://[^/]+/my-account/~i',\n                '~^https?://[^/]+/\\?wc-ajax=~i',\n                '~^https?://[^/]+/wp-trackback\\.php~i',\n            ],\n        ],\n        'joomla' => [\n            'label' => 'Joomla',\n            'patterns' => [\n                '~^https?://[^/]+/administrator/~i',\n                '~^https?://[^/]+/index\\.php\\?option=com_users&task=user\\.login~i',\n                '~^https?://[^/]+/index\\.php\\?option=com_users&task=user\\.logout~i',\n                '~^https?://[^/]+/index\\.php\\?option=com_users&task=registration~i',\n                '~^https?://[^/]+/index\\.php\\?option=com_contact&task=contact\\.submit~i',\n                '~[?&]format=feed~i',\n                '~[?&]format=json~i',\n                '~[?&]format=raw~i',\n                '~[?&]tmpl=component~i',\n                '~^https?://[^/]+/index\\.php\\?option=com_search~i',\n                '~^https?://[^/]+/index\\.php\\?option=com_finder~i',\n                '~^https?://[^/]+/index\\.php\\?option=com_ajax~i',\n                '~^https?://[^/]+/cache/~i',\n                '~^https?://[^/]+/tmp/~i',\n                '~^https?://[^/]+/logs/~i',\n                '~^https?://[^/]+/cli/~i',\n            ],\n        ],\n        'drupal' => [\n            'label' => 'Drupal',\n            'patterns' => [\n                '~^https?://[^/]+/admin/~i',\n                '~^https?://[^/]+/user/login~i',\n                '~^https?://[^/]+/user/logout~i',\n                '~^https?://[^/]+/user/register~i',\n                '~^https?://[^/]+/user/password~i',\n                '~^https?://[^/]+/\\?q=user/~i',\n                '~^https?://[^/]+/\\?q=admin/~i',\n                '~^https?://[^/]+/jsonapi/~i',\n                '~^https?://[^/]+/api/~i',\n                '~^https?://[^/]+/batch\\b~i',\n                '~[?&]ajax_form=1~i',\n                '~^https?://[^/]+/cron/~i',\n                '~^https?://[^/]+/update\\.php~i',\n                '~^https?://[^/]+/install\\.php~i',\n                '~^https?://[^/]+/rebuild\\.php~i',\n                '~^https?://[^/]+/core/rebuild\\.php~i',\n                '~^https?://[^/]+/sites/default/files/~i',\n            ],\n        ],\n    ],\n];\n"
  },
  {
    "path": "packages/crawler/database/migrations/2024_09_06_213000_create_web_crawlers_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Crawler\\Enums\\State;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('web_crawlers', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(Site::class)->nullable()->constrained()->onDelete('cascade');\n            $table->foreignIdFor(Team::class)->constrained()->onDelete('cascade');\n\n            $table->string('state')->default(State::Pending->value);\n            $table->string('schedule');\n\n            $table->string('start_url');\n            $table->json('sitemaps')->nullable();\n\n            $table->json('crawler_stats')->nullable();\n            $table->json('settings')->nullable();\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('web_crawlers');\n    }\n};\n"
  },
  {
    "path": "packages/crawler/database/migrations/2024_09_06_220000_create_web_crawled_urls_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('web_crawled_urls', function (Blueprint $table) {\n            $table->uuid();\n            $table->foreignIdFor(Crawler::class)->nullable()->constrained('web_crawlers')->onDelete('cascade');\n            $table->foreignIdFor(Team::class)->constrained()->onDelete('cascade');\n            $table->uuid('found_on_id')->nullable();\n\n            $table->boolean('crawled')->default(false);\n            $table->string('url', 2048);\n            $table->integer('status')->nullable();\n\n            $table->timestamps();\n\n            $table->index(['crawler_id', 'crawled']);\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('web_crawled_urls');\n    }\n};\n"
  },
  {
    "path": "packages/crawler/database/migrations/2025_01_18_220000_crawled_urls_url_length.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('web_crawled_urls', function (Blueprint $table) {\n            $table->string('url', 8192)->change();\n        });\n    }\n};\n"
  },
  {
    "path": "packages/crawler/database/migrations/2025_02_01_183000_web_crawlers_enabled_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('web_crawlers', function (Blueprint $table): void {\n            $table->boolean('enabled')->default(true)->after('id');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropColumns('web_crawlers', ['enabled']);\n    }\n};\n"
  },
  {
    "path": "packages/crawler/database/migrations/2025_04_07_200000_web_crawled_urls_url_hash.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('web_crawled_urls', function (Blueprint $table): void {\n            $table->string('url_hash')->after('url');\n\n            $table->index(['crawler_id', 'url_hash']);\n        });\n    }\n};\n"
  },
  {
    "path": "packages/crawler/database/migrations/2025_09_28_100000_create_web_crawler_ignored_urls_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('web_crawler_ignored_urls', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(Crawler::class)->nullable()->constrained('web_crawlers')->onDelete('cascade');\n\n            $table->string('url_hash');\n\n            $table->timestamps();\n\n            $table->unique(['crawler_id', 'url_hash']);\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('web_crawler_ignored_urls');\n    }\n};\n"
  },
  {
    "path": "packages/crawler/database/migrations/2025_09_29_190000_web_crawled_urls_ignored_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('web_crawled_urls', function (Blueprint $table): void {\n            $table->boolean('ignored')->default(false)->after('crawled')->index();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('web_crawled_urls', function (Blueprint $table): void {\n            $table->dropColumn('ignored');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/crawler/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n    treatPhpDocTypesAsCertain: false\n    ignoreErrors:\n        - identifier: missingType.iterableValue\n        - identifier: missingType.generics\n"
  },
  {
    "path": "packages/crawler/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/crawler/resources/navigation.php",
    "content": "<?php\n\nuse Vigilant\\Core\\Facades\\Navigation;\n\nNavigation::add(route('crawler.index'), 'Link Issues')\n    ->parent('health')\n    ->icon('carbon-text-link')\n    ->gate('use-crawler')\n    ->routeIs('crawler*')\n    ->sort(3);\n"
  },
  {
    "path": "packages/crawler/resources/views/components/empty-states/crawlers.blade.php",
    "content": "<x-frontend::empty-state\n    :title=\"__('No Crawlers Configured')\"\n    :description=\"__('Add a crawler to monitor your site structure, discover issues, and keep content healthy.')\"\n    icon=\"phosphor-warning-circle\"\n    iconClass=\"h-12 w-12 text-purple\"\n    iconWrapperClass=\"rounded-full bg-purple/10 p-4 mb-6\"\n    :buttonHref=\"route('crawler.create')\"\n    :buttonText=\"__('Add Crawler')\"\n    buttonClass=\"bg-purple hover:bg-purple/90 text-base-50 px-5 py-2.5 rounded-lg transition-all duration-300\"\n/>\n"
  },
  {
    "path": "packages/crawler/resources/views/crawler/index.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header :back=\"route('crawler.index')\" :title=\"'Crawler - ' . $crawler->start_url . ($crawler->enabled ? '' : ' (Disabled)')\">\n            <x-frontend::page-header.actions>\n                <x-form.button dusk=\"crawler-edit-button\" :href=\"route('crawler.edit', ['crawler' => $crawler])\">\n                    @lang('Edit')\n                </x-form.button>\n                <x-form.button class=\"bg-red\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.button>\n            </x-frontend::page-header.actions>\n\n            <x-frontend::page-header.mobile-actions>\n                <x-form.dropdown-button :href=\"route('crawler.edit', ['crawler' => $crawler])\">\n                    @lang('Edit')\n                </x-form.dropdown-button>\n                <x-form.dropdown-button class=\"!text-red hover:!text-red-light\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.dropdown-button>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    <livewire:crawler-dashboard :crawlerId=\"$crawler->id\" wire:key=\"crawher-dashboard\" />\n\n    @if ($crawler->state === \\Vigilant\\Crawler\\Enums\\State::Crawling)\n        <div class=\"mt-4\">\n            <h2 class=\"text-xl font-bold leading-7 sm:truncate sm:text-2xl sm:tracking-tight text-neutral-100 mb-2\">\n                {{ __('Crawling') }}</h2>\n\n            <livewire:crawler-crawled-urls-table :crawlerId=\"$crawler->id\" wire:key=\"crawled-urls-table\" />\n        </div>\n    @endif\n\n    <div class=\"mt-4\">\n        <h2 class=\"text-xl font-bold leading-7 sm:truncate sm:text-2xl sm:tracking-tight text-neutral-100 mb-2\">\n            {{ __('Issues') }}</h2>\n\n        <livewire:crawler-issues-table :crawlerId=\"$crawler->id\" wire:key=\"issues-table\" />\n    </div>\n\n    <!-- Delete Confirmation Modal -->\n    <div x-data=\"{ showDeleteModal: false }\" @open-delete-modal.window=\"showDeleteModal = true\">\n        <x-frontend::modal show=\"showDeleteModal\">\n            <x-frontend::modal.header icon=\"phosphor-trash\" iconColor=\"red\" show=\"showDeleteModal\">\n                @lang('Delete Crawler Monitor')\n            </x-frontend::modal.header>\n\n            <x-frontend::modal.body>\n                <div class=\"space-y-4\">\n                    <p class=\"text-base-100\">\n                        @lang('Are you sure you want to delete this crawler monitor?')\n                    </p>\n                    <div class=\"bg-base-850 border border-base-700 rounded-lg p-4\">\n                        <div class=\"flex items-start gap-3\">\n                            <div class=\"flex-shrink-0\">\n                                @svg('phosphor-warning-circle', 'w-5 h-5 text-orange mt-0.5')\n                            </div>\n                            <div class=\"flex-1\">\n                                <p class=\"text-sm text-base-300\">\n                                    <span class=\"font-semibold text-base-100\">{{ $crawler->start_url }}</span>\n                                </p>\n                                <p class=\"text-sm text-base-400 mt-1\">\n                                    @lang('This action cannot be undone. All crawl data and issues for this monitor will be permanently deleted.')\n                                </p>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </x-frontend::modal.body>\n\n            <x-frontend::modal.footer>\n                <x-form.button type=\"button\" @click=\"showDeleteModal = false\">\n                    @lang('Cancel')\n                </x-form.button>\n                <form action=\"{{ route('crawler.delete', ['crawler' => $crawler]) }}\" method=\"POST\" class=\"inline\">\n                    @csrf\n                    @method('DELETE')\n                    <x-form.button class=\"bg-red\" type=\"submit\">\n                        @lang('Delete Monitor')\n                    </x-form.button>\n                </form>\n            </x-frontend::modal.footer>\n        </x-frontend::modal>\n    </div>\n\n</x-app-layout>\n"
  },
  {
    "path": "packages/crawler/resources/views/crawlers.blade.php",
    "content": "<div>\n    <x-slot name=\"header\">\n        <x-page-header title=\"Crawlers\">\n            <x-frontend::page-header.actions>\n                <x-create-button dusk=\"crawler-add-button\" :href=\"route('crawler.create')\"\n                    model=\"Vigilant\\Crawler\\Models\\Crawler\">\n                    @lang('Add Crawler')\n                </x-create-button>\n            </x-frontend::page-header.actions>\n            <x-frontend::page-header.mobile-actions>\n                <x-create-button-dropdown :href=\"route('crawler.create')\" model=\"Vigilant\\Crawler\\Models\\Crawler\">\n                    @lang('Add Crawler')\n                </x-create-button-dropdown>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    @if ($hasCrawlers)\n        <livewire:crawler-table />\n    @else\n        <x-crawler::empty-states.crawlers />\n    @endif\n\n</div>\n"
  },
  {
    "path": "packages/crawler/resources/views/livewire/crawler/dashboard.blade.php",
    "content": "<dl class=\"grid grid-cols-2 lg:grid-cols-4 gap-4\">\n    <x-frontend::stats-card :title=\"__('URLs found')\">\n        {{ $total_url_count }}\n    </x-frontend::stats-card>\n\n    <x-frontend::stats-card :title=\"__($issue_count == 1 ? 'Issue found' : 'Issues found')\">\n        {{ $issue_count }}\n    </x-frontend::stats-card>\n\n    <x-frontend::stats-card :title=\"__('Next Run')\">\n        {{ $nextRun }}\n    </x-frontend::stats-card>\n\n    <x-frontend::stats-card :title=\"__('Ignored URLs')\">\n        {{ $ignored_count }}\n    </x-frontend::stats-card>\n</dl>\n"
  },
  {
    "path": "packages/crawler/resources/views/livewire/crawler-form.blade.php",
    "content": "<div>\n    @if (!$inline)\n        <x-slot name=\"header\">\n            <x-page-header :title=\"$updating ? __('Edit Crawler :url', ['url' => $crawler->start_url]) : __('Add Crawler')\" :back=\"$updating ? route('crawler.view', ['crawler' => $crawler]) : route('crawler.index')\">\n            </x-page-header>\n        </x-slot>\n    @endif\n\n    <form wire:submit=\"save\">\n        <div class=\"max-w-7xl mx-auto\">\n            <x-card>\n                <div class=\"flex flex-col gap-4\">\n                    @if (!$inline)\n                        <x-form.checkbox field=\"form.enabled\" name=\"Enabled\" description=\"Enable or disable the crawler\" />\n                    @endif\n\n                    <x-form.text field=\"form.start_url\" name=\"Start URL\" description=\"Enter the URL to start crawling\" />\n\n                    <x-form.text-list field=\"form.sitemaps\" name=\"Sitemaps\" description=\"Sitemaps to retrieve URLs from\"\n                        placeholder=\"Sitemap URL\" :items=\"$form->sitemaps\" />\n\n                    <div class=\"grid grid-cols-2\" x-data=\"{\n                        type: @entangle('form.settings.scheduleConfig.type'),\n                    }\">\n                        <div>\n                            <label for=\"schedule\"\n                                class=\"block text-sm font-medium leading-6 text-base-100\">@lang('Schedule')</label>\n                            <span class=\"text-base-400 text-xs\">@lang('Choose how often the website should be crawled')</span>\n                        </div>\n                        <div class=\"flex items-center gap-2\">\n                            <select x-model=\"type\"\n                                class=\"mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-base-100 bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red\">\n                                <option value=\"daily\">@lang('Daily')</option>\n                                <option value=\"weekly\">@lang('Weekly')</option>\n                                <option value=\"monthly\">@lang('Monthly')</option>\n                            </select>\n\n                            <span x-show=\"type !== 'daily'\" class=\"text-base-100\">@lang('On')</span>\n\n                            <select x-show=\"type === 'weekly'\" wire:model.live=\"form.settings.scheduleConfig.weekDay\"\n                                class=\"mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-base-100 bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red\">\n                                @foreach (['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] as $index => $day)\n                                    <option value=\"{{ $index }}\">{{ $day }}</option>\n                                @endforeach\n                            </select>\n\n                            <select x-show=\"type === 'monthly'\" wire:model.live=\"form.settings.scheduleConfig.monthDay\"\n                                class=\"mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-base-100 bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red\">\n                                @for ($i = 0; $i < 31; $i++)\n                                    <option value=\"{{ $i }}\">Day {{ $i + 1 }}</option>\n                                @endfor\n                            </select>\n\n                            <span class=\"text-base-100\">@lang('At')</span>\n\n                            <select wire:model.live=\"form.settings.scheduleConfig.hour\"\n                                class=\"mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-base-100 bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red\">\n                                @for ($i = 0; $i < 24; $i++)\n                                    <option value=\"{{ $i }}\">{{ sprintf('%02d', $i) }}:00</option>\n                                @endfor\n\n                            </select>\n\n                        </div>\n                        <div class=\"mt-1\">\n                            @error('schedule')\n                                <span class=\"text-red\">{{ $message }}</span>\n                            @enderror\n                            @if ($invalidDay)\n                                <span class=\"text-orange\">@lang('Warning! Setting the day above 28 will cause it to NOT run montly')</span>\n                            @endif\n                        </div>\n\n                    </div>\n\n\n                    @if (!$inline)\n                        <div x-data=\"{\n                            open: false,\n                            platform: '',\n                            platforms: {{ Js::from(collect(config('crawler.platform_blacklists'))->map(fn ($p) => ['label' => $p['label'], 'patterns' => implode(\"\\n\", $p['patterns'])])) }},\n                            applyPlatform() {\n                                if (this.platform === '') return;\n                                this.$refs.urlBlacklist.value = this.platforms[this.platform].patterns;\n                                this.$refs.urlBlacklist.dispatchEvent(new Event('input'));\n                                this.$refs.urlBlacklist.dispatchEvent(new Event('change'));\n                            },\n                        }\" class=\"flex flex-col gap-4\">\n                            <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                                <div class=\"flex flex-col justify-center\">\n                                    <label class=\"block text-base font-semibold leading-6 text-base-50\">@lang('Platform preset')</label>\n                                    <span class=\"text-base-400 text-sm mt-1\">@lang('Pre-fills the URL blacklist for the selected platform.')</span>\n                                </div>\n                                <div class=\"flex flex-col justify-center\">\n                                    <select x-model=\"platform\" @change=\"applyPlatform()\"\n                                        class=\"block w-full rounded-lg border-0 py-2.5 px-3 text-base-100 bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red transition-all duration-200\">\n                                        <option value=\"\">@lang('None')</option>\n                                        <template x-for=\"[key, p] in Object.entries(platforms)\" :key=\"key\">\n                                            <option :value=\"key\" x-text=\"p.label\"></option>\n                                        </template>\n                                    </select>\n                                </div>\n                            </div>\n\n                            <div class=\"border-t border-base-700 pt-4\">\n                                <button type=\"button\" @click=\"open = !open\"\n                                    class=\"flex items-center justify-between w-full text-sm font-medium text-base-400 hover:text-base-100 transition-colors duration-200\">\n                                    @lang('Advanced')\n                                    <div class=\"transition-transform duration-200\" x-bind:class=\"open ? 'rotate-180' : ''\">\n                                        @svg('phosphor-caret-down', 'w-4 h-4')\n                                    </div>\n                                </button>\n\n                                <div x-show=\"open\" x-cloak x-collapse class=\"mt-4 flex flex-col gap-4\">\n                                    <x-form.textarea\n                                        field=\"form.url_blacklist\"\n                                        name=\"URL Blacklist\"\n                                        description=\"One regex pattern per line. URLs matching any pattern will not be crawled.\"\n                                        :rows=\"6\"\n                                        placeholder=\"/admin/&#10;/private/.*\"\n                                        xRef=\"urlBlacklist\"\n                                    />\n                                </div>\n                            </div>\n\n                            <div class=\"flex justify-end gap-4\">\n                                <x-form.submit-button dusk=\"submit-button\" :submitText=\"$updating ? 'Save' : 'Create'\" />\n                            </div>\n                        </div>\n                    @endif\n                </div>\n            </x-card>\n        </div>\n    </form>\n</div>\n"
  },
  {
    "path": "packages/crawler/routes/web.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\Crawler\\Http\\Controllers\\CrawlerController;\nuse Vigilant\\Crawler\\Livewire\\CrawlerForm;\nuse Vigilant\\Crawler\\Livewire\\Crawlers;\n\nRoute::prefix('crawler')\n    ->middleware('can:use-crawler')\n    ->group(function (): void {\n        Route::get('', Crawlers::class)->name('crawler.index');\n        Route::get('/create', CrawlerForm::class)->name('crawler.create');\n        Route::get('/{crawler}', [CrawlerController::class, 'index'])->name('crawler.view');\n        Route::delete('/{crawler}', [CrawlerController::class, 'delete'])->name('crawler.delete')->can('delete,crawler');\n        Route::get('/{crawler}/edit', CrawlerForm::class)->name('crawler.edit');\n    });\n"
  },
  {
    "path": "packages/crawler/src/Actions/CollectCrawlerStats.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Actions;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\LazyCollection;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Crawler\\Models\\CrawledUrl;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Crawler\\Notifications\\UrlIssuesNotification;\n\nclass CollectCrawlerStats\n{\n    public function __construct(protected TeamService $teamService) {}\n\n    public function collect(Crawler $crawler, bool $shouldNotify = true): void\n    {\n        $this->teamService->setTeamById($crawler->team_id);\n\n        /** @var Collection<int, int> $statuses */\n        $statuses = $crawler\n            ->urls()\n            ->where('crawled', '=', true)\n            ->where('ignored', '=', false)\n            ->selectRaw('status, COUNT(*) AS count')\n            ->groupBy('status')\n            ->get()\n            ->pluck('count', 'status');\n\n        $stats = [\n            'total_url_count' => $statuses->sum(),\n            'statuses' => $statuses,\n            'issue_count' => $crawler->urls()\n                ->where('crawled', '=', true)\n                ->where('ignored', '=', false)\n                ->where(function (Builder $query): void {\n                    $query->where('status', '>=', 400)\n                        ->orWhere('status', '=', 0);\n                })\n                ->count(),\n        ];\n\n        $crawler->update([\n            'crawler_stats' => $stats,\n        ]);\n\n        $crawler\n            ->urls()\n            ->where('status', '=', 200)\n            ->whereDoesntHave('foundOn')\n            ->select(['web_crawled_urls.uuid'])\n            ->lazy()\n            ->chunk(1000)\n            ->each(fn (LazyCollection $urls) => CrawledUrl::query()->whereIn('uuid', $urls->pluck('uuid'))->delete());\n\n        if ($shouldNotify && $stats['issue_count'] > 0) {\n            UrlIssuesNotification::notify($crawler);\n        }\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Actions/CrawlUrl.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Actions;\n\nuse Illuminate\\Http\\Client\\ConnectionException;\nuse Illuminate\\Http\\Client\\Response;\nuse Illuminate\\Support\\Arr;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Str;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Crawler\\Enums\\State;\nuse Vigilant\\Crawler\\Models\\CrawledUrl;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Crawler\\Models\\IgnoredUrl;\nuse Vigilant\\Crawler\\Notifications\\RatelimitedNotification;\n\nclass CrawlUrl\n{\n    protected const MAX_REDIRECTS = 5;\n\n    public function __construct(protected TeamService $teamService) {}\n\n    public function crawl(CrawledUrl $url, int $try = 0): void\n    {\n        $this->teamService->setTeamById($url->team_id);\n\n        $canCreateCrawledUrl = Gate::check('create-crawled-url', $url->crawler);\n\n        if (! $canCreateCrawledUrl) {\n            $url->crawler->update([\n                'state' => State::Limited,\n            ]);\n\n            return;\n        }\n\n        $allowedHost = parse_url($url->url, PHP_URL_HOST);\n\n        if (! is_string($allowedHost)) {\n            $url->update([\n                'status' => 0,\n                'crawled' => true,\n            ]);\n\n            return;\n        }\n\n        try {\n            $response = $this->fetchResponse($url->url, $allowedHost);\n        } catch (ConnectionException) {\n            if ($try < 3) {\n                $this->crawl($url, $try + 1);\n\n                return;\n            }\n\n            $url->update([\n                'status' => 0,\n                'crawled' => true,\n            ]);\n\n            return;\n        }\n\n        $baseUrl = parse_url($url->url) ?: [];\n\n        if (! $response->successful()) {\n            $url->update([\n                'status' => $response->status(),\n                'crawled' => true,\n            ]);\n\n            if ($response->status() === 429 && $url->crawler !== null) {\n                $url->crawler->update([\n                    'state' => State::Ratelimited,\n                ]);\n\n                RatelimitedNotification::notify($url->crawler);\n            }\n\n            return;\n        }\n\n        $html = $response->body();\n\n        if (! isset($baseUrl['host'], $baseUrl['scheme'])) {\n            $url->update(['crawled' => true]);\n\n            return;\n        }\n\n        $links = $this->extractLinks($html, $baseUrl);\n        $queuedLinks = [];\n\n        foreach ($links as $link) {\n            if (strlen($link) > 8192) {\n                $link = substr($link, 0, 8192);\n            }\n\n            $hash = md5($link);\n\n            $queuedLinks[$hash] = [\n                'crawler_id' => $url->crawler_id,\n                'url_hash' => $hash,\n                'url' => $link,\n                'found_on_id' => $url->uuid,\n            ];\n        }\n\n        $existingLinks = CrawledUrl::query()\n            ->where('crawler_id', '=', $url->crawler_id)\n            ->whereIn('url_hash', Arr::pluck($queuedLinks, 'url_hash'))\n            ->pluck('url_hash')\n            ->all();\n\n        $blacklistPatterns = $this->buildBlacklistPatterns($url->crawler);\n\n        $queuedLinks = collect($queuedLinks)\n            ->reject(fn (array $record): bool => in_array($record['url_hash'], $existingLinks, true))\n            ->when($blacklistPatterns->isNotEmpty(), fn (Collection $links): Collection => $links\n                ->reject(fn (array $record): bool => $blacklistPatterns\n                    ->contains(fn (string $pattern): bool => @preg_match($pattern, $record['url']) === 1)\n                )\n            )\n            ->all();\n\n        if ($queuedLinks !== []) {\n            $timestamp = now();\n            $records = [];\n            $ignoredHashes = [];\n\n            if ($url->crawler_id !== null) {\n                $ignoredHashes = IgnoredUrl::query()\n                    ->where('crawler_id', '=', $url->crawler_id)\n                    ->whereIn('url_hash', array_keys($queuedLinks))\n                    ->pluck('url_hash')\n                    ->all();\n            }\n\n            foreach ($queuedLinks as $record) {\n                $records[] = [\n                    'uuid' => (string) Str::uuid(),\n                    'crawler_id' => $record['crawler_id'],\n                    'team_id' => $url->team_id,\n                    'url_hash' => $record['url_hash'],\n                    'url' => $record['url'],\n                    'found_on_id' => $record['found_on_id'],\n                    'ignored' => in_array($record['url_hash'], $ignoredHashes, true),\n                    'crawled' => false,\n                    'created_at' => $timestamp,\n                    'updated_at' => $timestamp,\n                ];\n            }\n\n            CrawledUrl::query()->insertOrIgnore($records);\n        }\n\n        $url->update([\n            'status' => $response->status(),\n            'crawled' => true,\n        ]);\n    }\n\n    protected function buildBlacklistPatterns(Crawler $crawler): \\Illuminate\\Support\\Collection\n    {\n        return collect(explode(\"\\n\", (string) ($crawler->settings['url_blacklist'] ?? '')))\n            ->map(fn (string $line): string => trim($line))\n            ->filter()\n            ->values();\n    }\n\n    protected function extractLinks(string $html, array $baseUrl): array\n    {\n        if ($html === '' || stripos($html, '<a') === false || ! isset($baseUrl['host'], $baseUrl['scheme'])) {\n            return [];\n        }\n\n        $pattern = '~<a\\b[^>]*\\bhref\\s*=\\s*(?:\"([^\"]*)\"|\\'([^\\']*)\\'|([^\\s>\"\\']+))~i';\n\n        if (! preg_match_all($pattern, $html, $matches, PREG_SET_ORDER)) {\n            return [];\n        }\n\n        $links = [];\n\n        foreach ($matches as $match) {\n            $href = $match[1] ?? $match[2] ?? $match[3] ?? '';\n            $href = html_entity_decode(trim($href), ENT_QUOTES | ENT_HTML5);\n\n            if ($href === '' || $href === '#') {\n                continue;\n            }\n\n            $lowerHref = strtolower($href);\n\n            if (str_starts_with($lowerHref, 'mailto:') || str_starts_with($lowerHref, 'tel:') || str_starts_with($lowerHref, 'javascript:')) {\n                continue;\n            }\n\n            if (! filter_var($href, FILTER_VALIDATE_URL)) {\n                $href = $this->resolveRelativeUrl($href, $baseUrl);\n            }\n\n            if (! filter_var($href, FILTER_VALIDATE_URL) || ! $this->isSameDomain($href, $baseUrl['host'])) {\n                continue;\n            }\n\n            $href = $this->withoutQuery($href);\n\n            $normalized = rtrim($href, '/#');\n\n            if ($normalized === '') {\n                continue;\n            }\n\n            $links[$normalized] = true;\n        }\n\n        return array_keys($links);\n    }\n\n    protected function fetchResponse(string $currentUrl, ?string $allowedDomain, int $redirectCount = 0): Response\n    {\n        $response = $this->sendRequest($currentUrl);\n\n        $nextUrl = $this->nextRedirectUrl($response, $currentUrl, $allowedDomain, $redirectCount);\n\n        if ($nextUrl !== null) {\n            return $this->fetchResponse($nextUrl, $allowedDomain, $redirectCount + 1);\n        }\n\n        return $response;\n    }\n\n    protected function sendRequest(string $url): Response\n    {\n        $timeout = config()->integer('crawler.timeout', 5);\n        $connectTimeout = config()->integer('crawler.connect_timeout', $timeout);\n\n        return Http::timeout($timeout)\n            ->connectTimeout($connectTimeout)\n            ->withOptions(['verify' => false, 'allow_redirects' => false])\n            ->withUserAgent(config('core.user_agent'))\n            ->get($url);\n    }\n\n    protected function nextRedirectUrl(Response $response, string $currentUrl, ?string $allowedDomain, int $redirectCount): ?string\n    {\n        if (! $response->redirect() || $allowedDomain === null || $redirectCount >= self::MAX_REDIRECTS) {\n            return null;\n        }\n\n        $redirectLocation = $response->header('Location');\n\n        if (is_array($redirectLocation)) {\n            $redirectLocation = $redirectLocation[0] ?? null;\n        }\n\n        if (! is_string($redirectLocation) || $redirectLocation === '') {\n            return null;\n        }\n\n        if (! filter_var($redirectLocation, FILTER_VALIDATE_URL)) {\n            $baseParts = parse_url($currentUrl);\n\n            if ($baseParts === false || ! isset($baseParts['scheme'], $baseParts['host'])) {\n                return null;\n            }\n\n            $redirectLocation = $this->resolveRelativeUrl($redirectLocation, $baseParts);\n        }\n\n        return $this->isSameDomain($redirectLocation, $allowedDomain)\n            ? $redirectLocation\n            : null;\n    }\n\n    protected function isSameDomain(string $url, string $domain): bool\n    {\n        $host = parse_url($url, PHP_URL_HOST);\n\n        if ($host === null || $host === false) {\n            return false;\n        }\n\n        $host = strtolower($host);\n        $domain = strtolower($domain);\n\n        // Match exact domain or proper subdomain (with dot boundary)\n        return $host === $domain || preg_match('/\\.'.preg_quote($domain, '/').'$/', $host);\n    }\n\n    protected function withoutQuery(string $url): string\n    {\n        $parts = parse_url($url);\n\n        if ($parts === false || ! isset($parts['scheme'], $parts['host'])) {\n            return $url;\n        }\n\n        $scheme = $parts['scheme'];\n        $host = $parts['host'];\n        $userInfo = '';\n\n        if (isset($parts['user'])) {\n            $userInfo = $parts['user'];\n\n            if (isset($parts['pass'])) {\n                $userInfo .= ':'.$parts['pass'];\n            }\n\n            $userInfo .= '@';\n        }\n\n        $port = isset($parts['port']) ? ':'.$parts['port'] : '';\n        $path = $parts['path'] ?? '';\n\n        if ($path === '') {\n            $path = '/';\n        }\n\n        return $scheme.'://'.$userInfo.$host.$port.$path;\n    }\n\n    protected function resolveRelativeUrl(string $relativeUrl, array $baseUrlParts): string\n    {\n        // If the relative URL starts with \"//\", it refers to a protocol-relative URL\n        if (strpos($relativeUrl, '//') === 0) {\n            return $baseUrlParts['scheme'].':'.$relativeUrl;\n        }\n\n        // If the relative URL starts with \"/\", it's an absolute path relative to the domain\n        if (strpos($relativeUrl, '/') === 0) {\n            return $baseUrlParts['scheme'].'://'.$baseUrlParts['host'].$relativeUrl;\n        }\n\n        // Otherwise, it's a relative path, resolve by appending to base path\n        $basePath = isset($baseUrlParts['path']) ? dirname($baseUrlParts['path']) : '';\n\n        if ($basePath === '/') {\n            $basePath = '';\n        }\n\n        return $baseUrlParts['scheme'].'://'.$baseUrlParts['host'].$basePath.'/'.ltrim($relativeUrl, '/');\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Actions/ImportSitemaps.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Actions;\n\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Http;\nuse Mtownsend\\XmlToArray\\XmlToArray;\nuse Vigilant\\Crawler\\Models\\CrawledUrl;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Crawler\\Models\\IgnoredUrl;\n\nclass ImportSitemaps\n{\n    protected array $processed = [];\n\n    public function import(Crawler $crawler): void\n    {\n        foreach ($crawler->sitemaps ?? [] as $sitemap) {\n            $this->processSitemap($crawler, $sitemap);\n        }\n    }\n\n    protected function processSitemap(Crawler $crawler, string $url): void\n    {\n        if (in_array($url, $this->processed, true)) {\n            return;\n        }\n\n        $this->processed[] = $url;\n\n        $response = Http::withHeaders([\n            'Accept' => 'application/xml',\n        ])->get($url);\n\n        if (! $response->successful()) {\n            return;\n        }\n\n        $xml = $response->body();\n        $parsed = XmlToArray::convert($xml, true);\n\n        $root = $parsed['@root'] ?? 'urlset';\n\n        if ($root === 'urlset') {\n            /** @var array<int, string> $urls */\n            $urls = $parsed['url'] ?? [];\n            $urls = collect($urls)->pluck('loc')->filter();\n\n            $this->storeUrls($crawler, $urls);\n        }\n\n        if ($root === 'sitemapindex') {\n            /** @var array<int, string> $sitemaps */\n            $sitemaps = $parsed['sitemap'] ?? [];\n\n            $nested = collect($sitemaps)->pluck('loc')->filter();\n\n            foreach ($nested as $nestedSitemapUrl) {\n                $this->processSitemap($crawler, $nestedSitemapUrl);\n            }\n        }\n    }\n\n    protected function storeUrls(Crawler $crawler, Collection $urls): void\n    {\n        $chunks = $urls->chunk(5000);\n\n        foreach ($chunks as $chunk) {\n            $existingUrls = $crawler->urls()->whereIn('url', $chunk)->pluck('url');\n\n            $newUrls = $chunk->diff($existingUrls);\n\n            if ($newUrls->isEmpty()) {\n                continue;\n            }\n\n            $timestamp = now();\n            $urlHashes = $newUrls->map(fn ($url): string => md5($url));\n            $ignoredHashes = $urlHashes->isEmpty()\n                ? []\n                : IgnoredUrl::query()\n                    ->where('crawler_id', '=', $crawler->id)\n                    ->whereIn('url_hash', $urlHashes->all())\n                    ->pluck('url_hash')\n                    ->all();\n\n            $crawler->urls()->insert(\n                $newUrls->map(function ($url) use ($crawler, $timestamp, $ignoredHashes): array {\n                    $hash = md5($url);\n\n                    return [\n                        'uuid' => (new CrawledUrl)->newUniqueId(),\n                        'crawler_id' => $crawler->id,\n                        'team_id' => $crawler->team_id,\n                        'url' => $url,\n                        'url_hash' => $hash,\n                        'ignored' => in_array($hash, $ignoredHashes, true),\n                        'created_at' => $timestamp,\n                        'updated_at' => $timestamp,\n                    ];\n                })->toArray()\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Actions/ProcessCrawlerState.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Actions;\n\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Crawler\\Enums\\State;\nuse Vigilant\\Crawler\\Events\\CrawlerFinishedEvent;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nclass ProcessCrawlerState\n{\n    public function __construct(\n        protected TeamService $teamService,\n        protected StartCrawler $starter,\n    ) {}\n\n    public function process(Crawler $crawler): void\n    {\n        $this->teamService->setTeamById($crawler->team_id);\n\n        if ($crawler->state === State::Pending) {\n            $this->starter->start($crawler);\n\n            return;\n        }\n\n        if ($crawler->state === State::Crawling && $this->finished($crawler)) {\n\n            $crawler->update([\n                'state' => State::Finished,\n            ]);\n\n            event(new CrawlerFinishedEvent($crawler));\n\n            return;\n        }\n    }\n\n    protected function finished(Crawler $crawler): bool\n    {\n        $crawledUrlCount = $crawler\n            ->urls()\n            ->where('crawled', '=', true)\n            ->count();\n\n        $uncrawledUrlCount = $crawler\n            ->urls()\n            ->where('crawled', '=', false)\n            ->count();\n\n        return $crawledUrlCount > 0 && $uncrawledUrlCount === 0;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Actions/StartCrawler.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Actions;\n\nuse Vigilant\\Crawler\\Enums\\State;\nuse Vigilant\\Crawler\\Jobs\\ImportSitemapsJob;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nclass StartCrawler\n{\n    public function start(Crawler $crawler): void\n    {\n        $crawler->urls()->delete();\n\n        $crawler->urls()->firstOrCreate([\n            'url' => $crawler->start_url,\n        ]);\n\n        if ($crawler->sitemaps !== null && count($crawler->sitemaps) > 0) {\n            ImportSitemapsJob::dispatch($crawler);\n        }\n\n        $crawler->update([\n            'state' => State::Crawling,\n            'crawler_stats' => null,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Commands/CollectCrawlerStatsCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Crawler\\Jobs\\CollectCrawlerStatsJob;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nclass CollectCrawlerStatsCommand extends Command\n{\n    protected $signature = 'crawler:stats {crawlerId}';\n\n    protected $description = 'Collect ';\n\n    public function handle(TeamService $teamService): int\n    {\n        /** @var int $crawlerId */\n        $crawlerId = $this->argument('crawlerId');\n\n        /** @var Crawler $crawler */\n        $crawler = Crawler::query()->withoutGlobalScopes()->findOrFail($crawlerId);\n\n        CollectCrawlerStatsJob::dispatch($crawler);\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Commands/CrawlUrlsCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Foundation\\Bus\\PendingDispatch;\nuse Vigilant\\Crawler\\Enums\\State;\nuse Vigilant\\Crawler\\Jobs\\CrawUrlJob;\nuse Vigilant\\Crawler\\Models\\CrawledUrl;\n\nclass CrawlUrlsCommand extends Command\n{\n    protected $signature = 'crawler:crawl';\n\n    protected $description = 'Crawl pending URLs';\n\n    public function handle(): int\n    {\n        $count = config()->integer('crawler.crawls_per_minute');\n\n        CrawledUrl::query()\n            ->withoutGlobalScopes()\n            ->where('crawled', '=', false)\n            ->whereHas('crawler', fn (Builder $query) => $query->withoutGlobalScopes()->where('state', '=', State::Crawling))\n            ->take($count)\n            ->get()\n            ->each(fn (CrawledUrl $url): PendingDispatch => CrawUrlJob::dispatch($url));\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Commands/ProcessCrawlerStatesCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Foundation\\Bus\\PendingDispatch;\nuse Vigilant\\Crawler\\Enums\\State;\nuse Vigilant\\Crawler\\Jobs\\ProcessCrawlerStateJob;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nclass ProcessCrawlerStatesCommand extends Command\n{\n    protected $signature = 'crawler:process';\n\n    protected $description = 'Process all crawlers';\n\n    public function handle(): int\n    {\n        Crawler::query()\n            ->withoutGlobalScopes()\n            ->whereIn('state', [\n                State::Pending,\n                State::Crawling,\n            ])\n            ->get()\n            ->each(fn (Crawler $crawler): PendingDispatch => ProcessCrawlerStateJob::dispatch($crawler));\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Commands/ScheduleCrawlersCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Commands;\n\nuse Cron\\CronExpression;\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Crawler\\Actions\\StartCrawler;\nuse Vigilant\\Crawler\\Enums\\State;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nclass ScheduleCrawlersCommand extends Command\n{\n    protected $signature = 'crawler:schedule';\n\n    protected $description = 'Schedule Crawling Jobs';\n\n    public function handle(StartCrawler $starter): int\n    {\n        Crawler::query()\n            ->withoutGlobalScopes()\n            ->where('enabled', '=', true)\n            ->where('state', '!=', State::Crawling)\n            ->get()\n            ->each(function (Crawler $crawler) use ($starter) {\n                if (CronExpression::isValidExpression($crawler->schedule)) {\n\n                    $expression = new CronExpression($crawler->schedule);\n\n                    if ($expression->isDue(now())) {\n                        $starter->start($crawler);\n                    }\n\n                }\n            });\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Commands/StartCrawlerCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Crawler\\Actions\\StartCrawler;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nclass StartCrawlerCommand extends Command\n{\n    protected $signature = 'crawler:start {crawlerId}';\n\n    protected $description = 'Start a crawler';\n\n    public function handle(StartCrawler $starter, TeamService $teamService): int\n    {\n        /** @var int $crawlerId */\n        $crawlerId = $this->argument('crawlerId');\n\n        /** @var Crawler $crawler */\n        $crawler = Crawler::query()->withoutGlobalScopes()->findOrFail($crawlerId);\n\n        $teamService->setTeamById($crawler->team_id);\n\n        $starter->start($crawler);\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Enums/State.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Enums;\n\nenum State: string\n{\n    case Pending = 'pending';\n    case Crawling = 'crawling';\n    case Finished = 'finished';\n\n    case Ratelimited = 'ratelimited';\n    case Limited = 'limited';\n    case Failed = 'failed';\n\n    public function label(): string\n    {\n        return match ($this) {\n            State::Pending => 'Pending',\n            State::Crawling => 'Crawling',\n            State::Finished => 'Finished',\n\n            State::Ratelimited => 'Rate Limited',\n            State::Limited => 'Limited',\n            State::Failed => 'Failed',\n        };\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Enums/Status.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Enums;\n\nenum Status: int\n{\n    case ConnectionFailure = 0;\n    case BAD_REQUEST = 400;\n    case UNAUTHORIZED = 401;\n    case PAYMENT_REQUIRED = 402;\n    case FORBIDDEN = 403;\n    case NOT_FOUND = 404;\n    case METHOD_NOT_ALLOWED = 405;\n    case NOT_ACCEPTABLE = 406;\n    case PROXY_AUTHENTICATION_REQUIRED = 407;\n    case REQUEST_TIMEOUT = 408;\n    case CONFLICT = 409;\n    case GONE = 410;\n    case LENGTH_REQUIRED = 411;\n    case PRECONDITION_FAILED = 412;\n    case PAYLOAD_TOO_LARGE = 413;\n    case URI_TOO_LONG = 414;\n    case UNSUPPORTED_MEDIA_TYPE = 415;\n    case RANGE_NOT_SATISFIABLE = 416;\n    case EXPECTATION_FAILED = 417;\n    case IM_A_TEAPOT = 418;\n    case MISDIRECTED_REQUEST = 421;\n    case UNPROCESSABLE_ENTITY = 422;\n    case LOCKED = 423;\n    case FAILED_DEPENDENCY = 424;\n    case TOO_EARLY = 425;\n    case UPGRADE_REQUIRED = 426;\n    case PRECONDITION_REQUIRED = 428;\n    case RATE_LIMITED = 429;\n    case REQUEST_HEADER_FIELDS_TOO_LARGE = 431;\n    case UNAVAILABLE_FOR_LEGAL_REASONS = 451;\n    case INTERNAL_SERVER_ERROR = 500;\n    case NOT_IMPLEMENTED = 501;\n    case BAD_GATEWAY = 502;\n    case SERVICE_UNAVAILABLE = 503;\n    case GATEWAY_TIMEOUT = 504;\n    case HTTP_VERSION_NOT_SUPPORTED = 505;\n    case VARIANT_ALSO_NEGOTIATES = 506;\n    case INSUFFICIENT_STORAGE = 507;\n    case LOOP_DETECTED = 508;\n    case NOT_EXTENDED = 510;\n    case NETWORK_AUTHENTICATION_REQUIRED = 511;\n\n    // Cloudflare-specific status codes\n    case CLOUDFLARE_UNKNOWN_ERROR = 520;\n    case CLOUDFLARE_WEB_SERVER_DOWN = 521;\n    case CLOUDFLARE_CONNECTION_TIMED_OUT = 522;\n    case CLOUDFLARE_ORIGIN_UNREACHABLE = 523;\n    case CLOUDFLARE_TIMEOUT = 524;\n    case CLOUDFLARE_SSL_HANDSHAKE_FAILED = 525;\n    case CLOUDFLARE_INVALID_SSL_CERTIFICATE = 526;\n    case CLOUDFLARE_RAILGUN_ERROR = 527;\n    case CLOUDFLARE_ERROR = 530;\n\n    public function label(): string\n    {\n        return match ($this) {\n            Status::ConnectionFailure => 'Failed to connect',\n            Status::BAD_REQUEST => 'Bad Request',\n            Status::UNAUTHORIZED => 'Unauthorized',\n            Status::PAYMENT_REQUIRED => 'Payment Required',\n            Status::FORBIDDEN => 'Forbidden',\n            Status::NOT_FOUND => 'Not Found',\n            Status::METHOD_NOT_ALLOWED => 'Method Not Allowed',\n            Status::NOT_ACCEPTABLE => 'Not Acceptable',\n            Status::PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required',\n            Status::REQUEST_TIMEOUT => 'Request Timeout',\n            Status::CONFLICT => 'Conflict',\n            Status::GONE => 'Gone',\n            Status::LENGTH_REQUIRED => 'Length Required',\n            Status::PRECONDITION_FAILED => 'Precondition Failed',\n            Status::PAYLOAD_TOO_LARGE => 'Payload Too Large',\n            Status::URI_TOO_LONG => 'URI Too Long',\n            Status::UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type',\n            Status::RANGE_NOT_SATISFIABLE => 'Range Not Satisfiable',\n            Status::EXPECTATION_FAILED => 'Expectation Failed',\n            Status::IM_A_TEAPOT => \"I'm a Teapot\",\n            Status::MISDIRECTED_REQUEST => 'Misdirected Request',\n            Status::UNPROCESSABLE_ENTITY => 'Unprocessable Entity',\n            Status::LOCKED => 'Locked',\n            Status::FAILED_DEPENDENCY => 'Failed Dependency',\n            Status::TOO_EARLY => 'Too Early',\n            Status::UPGRADE_REQUIRED => 'Upgrade Required',\n            Status::PRECONDITION_REQUIRED => 'Precondition Required',\n            Status::RATE_LIMITED => 'Rate Limited',\n            Status::REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large',\n            Status::UNAVAILABLE_FOR_LEGAL_REASONS => 'Unavailable For Legal Reasons',\n            Status::INTERNAL_SERVER_ERROR => 'Internal Server Error',\n            Status::NOT_IMPLEMENTED => 'Not Implemented',\n            Status::BAD_GATEWAY => 'Bad Gateway',\n            Status::SERVICE_UNAVAILABLE => 'Service Unavailable',\n            Status::GATEWAY_TIMEOUT => 'Gateway Timeout',\n            Status::HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version Not Supported',\n            Status::VARIANT_ALSO_NEGOTIATES => 'Variant Also Negotiates',\n            Status::INSUFFICIENT_STORAGE => 'Insufficient Storage',\n            Status::LOOP_DETECTED => 'Loop Detected',\n            Status::NOT_EXTENDED => 'Not Extended',\n            Status::NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required',\n\n            // Cloudflare-specific status codes\n            Status::CLOUDFLARE_UNKNOWN_ERROR => 'Unknown Error (Cloudflare)',\n            Status::CLOUDFLARE_WEB_SERVER_DOWN => 'Web Server Is Down (Cloudflare)',\n            Status::CLOUDFLARE_CONNECTION_TIMED_OUT => 'Connection Timed Out (Cloudflare)',\n            Status::CLOUDFLARE_ORIGIN_UNREACHABLE => 'Origin Is Unreachable (Cloudflare)',\n            Status::CLOUDFLARE_TIMEOUT => 'A Timeout Occurred (Cloudflare)',\n            Status::CLOUDFLARE_SSL_HANDSHAKE_FAILED => 'SSL Handshake Failed (Cloudflare)',\n            Status::CLOUDFLARE_INVALID_SSL_CERTIFICATE => 'Invalid SSL Certificate (Cloudflare)',\n            Status::CLOUDFLARE_RAILGUN_ERROR => 'Railgun Error (Cloudflare)',\n            Status::CLOUDFLARE_ERROR => 'Cloudflare Error',\n        };\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Events/CrawlerFinishedEvent.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Events;\n\nuse Illuminate\\Foundation\\Events\\Dispatchable;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nclass CrawlerFinishedEvent\n{\n    use Dispatchable;\n    use SerializesModels;\n\n    public function __construct(public Crawler $crawler) {}\n}\n"
  },
  {
    "path": "packages/crawler/src/Exports/IssuesExport.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Exports;\n\nuse Illuminate\\Support\\Collection;\nuse Maatwebsite\\Excel\\Concerns\\FromCollection;\nuse Maatwebsite\\Excel\\Concerns\\WithHeadings;\nuse Maatwebsite\\Excel\\Concerns\\WithMapping;\nuse Vigilant\\Crawler\\Enums\\Status;\nuse Vigilant\\Crawler\\Models\\CrawledUrl;\n\nclass IssuesExport implements FromCollection, WithHeadings, WithMapping\n{\n    public function __construct(\n        protected Collection $collection\n    ) {}\n\n    public function collection(): Collection\n    {\n        return $this->collection;\n    }\n\n    public function headings(): array\n    {\n        return [\n            'URL',\n            'Status Code',\n            'Status',\n            'Found On',\n            'Ignored',\n        ];\n    }\n\n    public function map(mixed $row): array\n    {\n        /** @var CrawledUrl $row */\n        return [\n            $row->url,\n            $row->status,\n            Status::tryFrom($row->status)?->label() ?? (string) $row->status,\n            $row->foundOn->url ?? '',\n            $row->ignored ? 'Yes' : 'No',\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Http/Controllers/CrawlerController.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Http\\Controllers;\n\nuse Illuminate\\Routing\\Controller;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\n\nclass CrawlerController extends Controller\n{\n    use DisplaysAlerts;\n\n    public function index(Crawler $crawler): mixed\n    {\n        /** @var view-string $view */\n        $view = 'crawler::crawler.index';\n\n        return view($view, [\n            'crawler' => $crawler,\n        ]);\n    }\n\n    public function delete(Crawler $crawler): mixed\n    {\n        $crawler->delete();\n\n        $this->alert(\n            __('Deleted'),\n            __('Crawler was successfully deleted'),\n            AlertType::Success\n        );\n\n        return response()->redirectToRoute('crawler.index');\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Jobs/CollectCrawlerStatsJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUniqueUntilProcessing;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Crawler\\Actions\\CollectCrawlerStats;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nclass CollectCrawlerStatsJob implements ShouldBeUniqueUntilProcessing, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(\n        public Crawler $crawler,\n        public bool $shouldNotify = true,\n    ) {\n        $this->onQueue(config('crawler.queue'));\n    }\n\n    public function handle(CollectCrawlerStats $crawlerStats): void\n    {\n        $crawlerStats->collect($this->crawler, $this->shouldNotify);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->crawler->id;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Jobs/CrawUrlJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Crawler\\Actions\\CrawlUrl;\nuse Vigilant\\Crawler\\Models\\CrawledUrl;\n\nclass CrawUrlJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(\n        public CrawledUrl $url\n    ) {\n        $this->onQueue(config('crawler.queue'));\n    }\n\n    public function handle(CrawlUrl $url): void\n    {\n        $url->crawl($this->url);\n    }\n\n    public function tags(): array\n    {\n        return [\n            $this->url->uuid,\n            $this->url->url,\n        ];\n    }\n\n    public function uniqueId(): string\n    {\n        return $this->url->uuid;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Jobs/ImportSitemapsJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Crawler\\Actions\\ImportSitemaps;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nclass ImportSitemapsJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(\n        public Crawler $crawler\n    ) {\n        $this->onQueue(config('crawler.queue'));\n    }\n\n    public function handle(ImportSitemaps $sitemaps): void\n    {\n        $sitemaps->import($this->crawler);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->crawler->id;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Jobs/ProcessCrawlerStateJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Crawler\\Actions\\ProcessCrawlerState;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nclass ProcessCrawlerStateJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(\n        public Crawler $crawler\n    ) {\n        $this->onQueue(config('crawler.queue'));\n    }\n\n    public function handle(ProcessCrawlerState $state): void\n    {\n        $state->process($this->crawler);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->crawler->id;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Jobs/StartCrawlerJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Crawler\\Actions\\StartCrawler;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nclass StartCrawlerJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(public Crawler $crawler)\n    {\n        $this->onQueue(config('crawler.queue'));\n    }\n\n    public function handle(StartCrawler $starter, TeamService $teamService): void\n    {\n        $teamService->setTeamById($this->crawler->team_id);\n        $starter->start($this->crawler);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->crawler->id;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Listeners/CrawlerFinishedListener.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Listeners;\n\nuse Vigilant\\Crawler\\Events\\CrawlerFinishedEvent;\nuse Vigilant\\Crawler\\Jobs\\CollectCrawlerStatsJob;\n\nclass CrawlerFinishedListener\n{\n    public function handle(CrawlerFinishedEvent $event): void\n    {\n        CollectCrawlerStatsJob::dispatch($event->crawler);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Livewire/Crawler/Dashboard.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Livewire\\Crawler;\n\nuse Cron\\CronExpression;\nuse Illuminate\\Support\\Carbon;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nclass Dashboard extends Component\n{\n    #[Locked]\n    public int $crawlerId;\n\n    public function mount(int $crawlerId): void\n    {\n        $this->crawlerId = $crawlerId;\n    }\n\n    public function render(): mixed\n    {\n        /** @var Crawler $crawler */\n        $crawler = Crawler::query()->findOrFail($this->crawlerId);\n\n        $nextRun = Carbon::parse((new CronExpression($crawler->schedule))->getNextRunDate());\n\n        /** @var view-string $view */\n        $view = 'crawler::livewire.crawler.dashboard';\n\n        return view($view, [\n            'total_url_count' => $crawler->totalUrlCount(),\n            'issue_count' => $crawler->issueCount() ?? 0,\n            'ignored_count' => $crawler->urls()->where('ignored', '=', true)->count(),\n            'nextRun' => $crawler->enabled ? $nextRun->diffForHumans() : __('Crawler disabled'),\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Livewire/CrawlerForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Livewire;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Frontend\\Traits\\CanBeInline;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass CrawlerForm extends Component\n{\n    use CanBeInline;\n    use DisplaysAlerts;\n\n    public Forms\\CrawlerForm $form;\n\n    #[Locked]\n    public Crawler $crawler;\n\n    public function mount(?Crawler $crawler, ?int $siteId = null): void\n    {\n        if ($crawler !== null && $crawler->exists) {\n            $this->authorize('update', $crawler);\n\n            $this->form->fill($crawler->toArray());\n            $this->form->url_blacklist = $crawler->settings['url_blacklist'] ?? '';\n        } else {\n            $this->authorize('create', Crawler::class);\n\n            if ($siteId !== null) {\n                /** @var Site $site */\n                $site = Site::query()->findOrFail($siteId);\n\n                $this->form->start_url = $site->url;\n                $this->form->site_id = $siteId;\n            }\n        }\n\n        if ($crawler !== null) {\n            $this->crawler = $crawler;\n        }\n    }\n\n    public function addListItem(string $field): void\n    {\n        if ($field === 'form.sitemaps') {\n            $this->form->sitemaps[] = '';\n        }\n    }\n\n    #[On('save')]\n    public function save(): void\n    {\n        $this->form->sitemaps = $this->form->sitemaps !== null ? array_filter($this->form->sitemaps) : null;\n        $this->form->schedule = $this->getCronSchedule();\n        $this->form->settings = array_merge($this->form->settings ?? [], [\n            'url_blacklist' => $this->form->url_blacklist,\n        ]);\n\n        $this->validate();\n\n        /** @var array<string, mixed> $formData */\n        $formData = $this->form->all();\n        $data = collect($formData)->except('url_blacklist')->all();\n\n        if ($this->crawler->exists) {\n            $this->authorize('update', $this->crawler);\n\n            $this->crawler->update($data);\n        } else {\n            $this->authorize('create', $this->crawler);\n\n            $this->crawler = Crawler::query()->create($data);\n        }\n\n        if (! $this->inline) {\n            $this->alert(\n                __('Saved'),\n                __('Crawler was successfully :action',\n                    ['action' => $this->crawler->wasRecentlyCreated ? 'created' : 'saved']),\n                AlertType::Success\n            );\n            $this->redirectRoute('crawler.view', ['crawler' => $this->crawler]);\n        }\n    }\n\n    protected function getCronSchedule(): string\n    {\n        $type = $this->form->settings['scheduleConfig']['type'] ?? 'montly';\n        $hour = $this->form->settings['scheduleConfig']['hour'] ?? 0;\n        $weekDay = $this->form->settings['scheduleConfig']['weekDay'] ?? 0;\n        $monthDay = $this->form->settings['scheduleConfig']['monthDay'] ?? 0;\n\n        return match ($type) {\n            'daily' => \"0 $hour * * *\",\n            'weekly' => \"0 $hour * * $weekDay\",\n            default => \"0 $hour $monthDay * *\",\n        };\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'crawler::livewire.crawler-form';\n\n        return view($view, [\n            'updating' => $this->crawler->exists,\n            'invalidDay' => ($this->form->settings['scheduleConfig']['monthDay'] ?? 0) > 28,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Livewire/Crawlers.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Livewire;\n\nuse Illuminate\\Contracts\\View\\View;\nuse Livewire\\Component;\nuse Vigilant\\Crawler\\Models\\Crawler as CrawlerModel;\n\nclass Crawlers extends Component\n{\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'crawler::crawlers';\n        $hasCrawlers = CrawlerModel::query()->exists();\n\n        return view($view, [\n            'hasCrawlers' => $hasCrawlers,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Livewire/Forms/CrawlerForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Form;\nuse Vigilant\\Core\\Validation\\CanEnableRule;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Crawler\\Validation\\EqualDomainRule;\nuse Vigilant\\Crawler\\Validation\\ValidRegexLines;\nuse Vigilant\\Frontend\\Validation\\CronExpression;\n\nclass CrawlerForm extends Form\n{\n    #[Locked]\n    public ?int $site_id;\n\n    public bool $enabled = true;\n\n    public string $schedule = '0 0 * * *';\n\n    public string $start_url = '';\n\n    public ?array $sitemaps = [];\n\n    public string $url_blacklist = '';\n\n    public ?array $settings = [\n        'scheduleConfig' => [\n            'type' => 'monthly',\n            'hour' => '9',\n            'weekDay' => 1,\n            'monthDay' => 1,\n        ],\n    ];\n\n    public function rules(): array\n    {\n        return [\n            'schedule' => ['required', new CronExpression],\n            'start_url' => ['required', 'max:255', 'url'],\n            'sitemaps' => ['required_without:start_url', 'array', new EqualDomainRule],\n            'sitemaps.*' => ['required', 'url'],\n            'settings' => ['array'],\n            'enabled' => ['boolean', new CanEnableRule(Crawler::class)],\n            'url_blacklist' => ['nullable', 'string', new ValidRegexLines],\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Livewire/Tables/CrawledUrlsTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Livewire\\Attributes\\Locked;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse RamonRietdijk\\LivewireTables\\Filters\\SelectFilter;\nuse Vigilant\\Crawler\\Models\\CrawledUrl;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\LinkColumn;\n\nclass CrawledUrlsTable extends BaseTable\n{\n    protected string $model = CrawledUrl::class;\n\n    public array $filters = [\n        'crawled' => '',\n    ];\n\n    #[Locked]\n    public int $crawlerId;\n\n    public function mount(int $crawlerId): void\n    {\n        $this->crawlerId = $crawlerId;\n    }\n\n    protected function columns(): array\n    {\n        return [\n            LinkColumn::make(__('URL'), 'url')\n                ->openInNewTab()\n                ->searchable()\n                ->sortable(),\n\n            Column::make(__('Crawled'), 'crawled')\n                ->displayUsing(fn (bool $crawled): string => $crawled ? __('Yes') : __('No'))\n                ->sortable(),\n        ];\n    }\n\n    protected function filters(): array\n    {\n        return [\n            SelectFilter::make(__('Crawled'), 'crawled')\n                ->options([\n                    'yes' => __('Crawled'),\n                    'no' => __('Not crawled'),\n                ])\n                ->filterUsing(function (Builder $builder, ?string $value): void {\n                    if ($value === 'yes') {\n                        $builder->where($builder->qualifyColumn('crawled'), '=', true);\n                    } elseif ($value === 'no') {\n                        $builder->where($builder->qualifyColumn('crawled'), '=', false);\n                    }\n                }),\n        ];\n    }\n\n    protected function query(): Builder\n    {\n        return parent::query()\n            ->where('web_crawled_urls.crawler_id', '=', $this->crawlerId);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Livewire/Tables/CrawlerTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Livewire\\Tables;\n\nuse Cron\\CronExpression;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Enumerable;\nuse Illuminate\\Support\\Facades\\Gate;\nuse InvalidArgumentException;\nuse RamonRietdijk\\LivewireTables\\Actions\\Action;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse RamonRietdijk\\LivewireTables\\Filters\\SelectFilter;\nuse Vigilant\\Crawler\\Actions\\StartCrawler;\nuse Vigilant\\Crawler\\Enums\\State;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Frontend\\Integrations\\Table\\Actions\\InlineAction;\nuse Vigilant\\Frontend\\Integrations\\Table\\ActionsColumn;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\Concerns\\HasInlineActions;\nuse Vigilant\\Frontend\\Integrations\\Table\\Enums\\Status;\nuse Vigilant\\Frontend\\Integrations\\Table\\StatusColumn;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass CrawlerTable extends BaseTable\n{\n    use HasInlineActions;\n\n    protected string $model = Crawler::class;\n\n    protected array $pollingOptions = [\n        '' => 'None',\n        '10s' => 'Every 10 seconds',\n    ];\n\n    protected function columns(): array\n    {\n        return [\n            StatusColumn::make(__('Status'))\n                ->text(function (Crawler $crawler): string {\n                    return $crawler->enabled ? __('Enabled') : __('Disabled');\n                })\n                ->status(function (Crawler $crawler): Status {\n                    return $crawler->enabled ? Status::Success : Status::Danger;\n                }),\n\n            Column::make(__('URL'), 'start_url')\n                ->searchable()\n                ->sortable(),\n\n            Column::make(__('Next Run'), 'schedule')\n                ->displayUsing(function (string $schedule, Crawler $crawler): ?string {\n                    if (! $crawler->enabled) {\n                        return null;\n                    }\n\n                    try {\n                        $expression = new CronExpression($schedule);\n                    } catch (InvalidArgumentException) {\n                        return null;\n                    }\n                    $nextRun = Carbon::parse($expression->getNextRunDate());\n\n                    return $nextRun->diffForHumans();\n                })\n                ->sortable(),\n\n            Column::make(__('Status'), 'state')\n                ->displayUsing(fn (State $state): string => __($state->label()))\n                ->sortable(),\n\n            StatusColumn::make(__('Issues'))\n                ->text(function (Crawler $crawler): string {\n                    $issueCount = $crawler->issueCount() ?? 0;\n\n                    return trans_choice(\n                        ':count issue|:count issues',\n                        $issueCount,\n                        ['count' => $issueCount]\n                    );\n                })\n                ->status(function (Crawler $crawler): Status {\n                    $count = $crawler->issueCount();\n\n                    if ($count === null || $count === 0) {\n                        return Status::Success;\n                    }\n\n                    $total = $crawler->totalUrlCount();\n\n                    $threshold = $total * 0.05;\n\n                    return $count > $threshold\n                        ? Status::Danger\n                        : Status::Warning;\n                }),\n\n            Column::make(__('URLs crawled'), function (Crawler $crawler): string {\n                return sprintf(\n                    '%d / %d',\n                    $crawler->urls()->where('crawled', '=', true)->count(),\n                    $crawler->urls()->count(),\n                );\n            }),\n\n            ActionsColumn::make(__('Actions'))\n                ->actions([\n                    InlineAction::make('start', __('Start crawler'), 'phosphor-play-bold')\n                        ->visible(fn (Crawler $crawler): bool => $crawler->state !== State::Crawling && $crawler->enabled),\n                ]),\n        ];\n    }\n\n    protected function filters(): array\n    {\n        return [\n            SelectFilter::make(__('Site'), 'site_id')\n                ->options(\n                    Site::query()\n                        ->orderBy('url')\n                        ->pluck('url', 'id')\n                        ->toArray()\n                ),\n        ];\n    }\n\n    protected function actions(): array\n    {\n        return [\n            Action::make(__('Start Crawler'), function (Enumerable $models): void {\n                /** @var StartCrawler $starter */\n                $starter = app(StartCrawler::class);\n\n                $models\n                    ->where('state', '!=', State::Crawling)\n                    ->each(fn (Crawler $crawler) => $starter->start($crawler));\n            }, 'start'),\n\n            Action::make(__('Enable'), function (Enumerable $models): void {\n                foreach ($models as $model) {\n                    if (! Gate::allows('create', $model)) {\n                        break;\n                    }\n\n                    $model->update(['enabled' => true]);\n                }\n            }, 'enable'),\n\n            Action::make(__('Disable'), function (Enumerable $models): void {\n                $models->each(fn (Crawler $crawler) => $crawler->update(['enabled' => false]));\n            }, 'disable'),\n\n            Action::make(__('Delete'), function (Enumerable $models): void {\n                $models->each(fn (Crawler $crawler): ?bool => $crawler->delete());\n            }, 'delete'),\n        ];\n    }\n\n    protected function link(Model $model): ?string\n    {\n        return route('crawler.view', ['crawler' => $model]);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Livewire/Tables/IssuesTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Enumerable;\nuse Illuminate\\Support\\Str;\nuse Livewire\\Attributes\\Locked;\nuse Maatwebsite\\Excel\\Facades\\Excel;\nuse RamonRietdijk\\LivewireTables\\Actions\\Action;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse RamonRietdijk\\LivewireTables\\Filters\\SelectFilter;\nuse Symfony\\Component\\HttpFoundation\\BinaryFileResponse;\nuse Vigilant\\Crawler\\Enums\\Status;\nuse Vigilant\\Crawler\\Exports\\IssuesExport;\nuse Vigilant\\Crawler\\Jobs\\CollectCrawlerStatsJob;\nuse Vigilant\\Crawler\\Models\\CrawledUrl;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Crawler\\Models\\IgnoredUrl;\nuse Vigilant\\Frontend\\Integrations\\Table\\Actions\\InlineAction;\nuse Vigilant\\Frontend\\Integrations\\Table\\ActionsColumn;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\Concerns\\HasInlineActions;\nuse Vigilant\\Frontend\\Integrations\\Table\\LinkColumn;\n\nclass IssuesTable extends BaseTable\n{\n    use HasInlineActions;\n\n    public array $filters = [\n        'ignored_urls' => 'hide',\n    ];\n\n    protected string $model = CrawledUrl::class;\n\n    #[Locked]\n    public int $crawlerId;\n\n    public function mount(int $crawlerId): void\n    {\n        $this->crawlerId = $crawlerId;\n    }\n\n    protected function columns(): array\n    {\n        return [\n            LinkColumn::make(__('URL'), 'url')\n                ->openInNewTab()\n                ->searchable()\n                ->sortable(),\n\n            Column::make(__('Status'), 'status')\n                ->displayUsing(fn (int $status): string => Status::tryFrom($status)?->label() ?? (string) $status)\n                ->sortable(),\n\n            LinkColumn::make(__('Found On'), 'foundOn.url')\n                ->openInNewTab()\n                ->searchable()\n                ->sortable(),\n\n            ActionsColumn::make(__('Actions'))\n                ->actions([\n                    InlineAction::make('ignoreUrl', __('Ignore'), 'phosphor-eye-slash-light')\n                        ->visible(fn (CrawledUrl $url): bool => ! $url->ignored),\n\n                    InlineAction::make('unignoreUrl', __('Unignore'), 'phosphor-eye-light')\n                        ->visible(fn (CrawledUrl $url): bool => $url->ignored),\n                ]),\n\n        ];\n    }\n\n    protected function actions(): array\n    {\n        return [\n            Action::make(__('Export All'), function (): BinaryFileResponse {\n                $collection = $this->appliedQuery()->with('foundOn')->get();\n\n                return Excel::download(\n                    new IssuesExport($collection),\n                    $this->generateFilename(),\n                );\n            })->standalone(),\n\n            Action::make(__('Export Selected'), function (Enumerable $models): BinaryFileResponse {\n                return Excel::download(\n                    new IssuesExport($models->collect()),\n                    $this->generateFilename(),\n                );\n            }),\n\n            Action::make(__('Ignore Selected'), function (Enumerable $models): void {\n                foreach ($models as $model) {\n                    IgnoredUrl::firstOrCreate([\n                        'crawler_id' => $this->crawlerId,\n                        'url_hash' => $model->url_hash,\n                    ]);\n                    $model->update(['ignored' => true]);\n                }\n\n                CollectCrawlerStatsJob::dispatch(Crawler::query()->findOrFail($this->crawlerId), false);\n            }, 'ignoreUrl'),\n\n            Action::make(__('Unignore Selected'), function (Enumerable $models): void {\n                foreach ($models as $model) {\n                    IgnoredUrl::query()\n                        ->where('crawler_id', '=', $this->crawlerId)\n                        ->where('url_hash', '=', $model->url_hash)\n                        ->delete();\n\n                    $model->ignored = false;\n                    $model->save();\n                }\n\n                CollectCrawlerStatsJob::dispatch(Crawler::query()->findOrFail($this->crawlerId), false);\n            }, 'unignoreUrl'),\n        ];\n    }\n\n    protected function generateFilename(): string\n    {\n        $crawler = Crawler::query()->with('site')->findOrFail($this->crawlerId);\n        $host = $crawler->site?->url ? parse_url($crawler->site->url, PHP_URL_HOST) : null;\n        $domain = Str::slug($host ?: 'unknown');\n        $date = now()->format('Y-m-d');\n\n        return \"{$domain}-broken-links-{$date}.csv\";\n    }\n\n    protected function filters(): array\n    {\n        return [\n            SelectFilter::make(__('Ignored URLs'), 'ignored_urls')\n                ->options([\n                    'hide' => __('Hide Ignored'),\n                    'only' => __('Only Ignored'),\n                ])\n                ->filterUsing(function (Builder $builder, ?string $value): void {\n                    if ($value === 'hide') {\n                        $builder->where($builder->qualifyColumn('ignored'), '=', false);\n                    } elseif ($value === 'only') {\n                        $builder->where($builder->qualifyColumn('ignored'), '=', true);\n                    }\n                }),\n        ];\n    }\n\n    protected function query(): Builder\n    {\n        return parent::query()\n            ->where('web_crawled_urls.crawler_id', '=', $this->crawlerId)\n            ->where(function (Builder $query): void {\n                $query->where('web_crawled_urls.status', '>=', 400)\n                    ->orWhere('web_crawled_urls.status', '=', 0);\n            });\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Models/CrawledUrl.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Concerns\\HasUuids;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Crawler\\Observers\\CrawledUrlObserver;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Observers\\TeamObserver;\n\n/**\n * @property string $uuid\n * @property ?int $crawler_id\n * @property int $team_id\n * @property int $found_on_id\n * @property bool $crawled\n * @property bool $ignored\n * @property string $url\n * @property string $url_hash\n * @property int $status\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Site $site\n * @property Crawler $crawler\n * @property ?CrawledUrl $foundOn\n * @property ?Team $team\n */\n#[ObservedBy([TeamObserver::class, CrawledUrlObserver::class])]\n#[ScopedBy(TeamScope::class)]\nclass CrawledUrl extends Model\n{\n    use HasUuids;\n\n    protected $primaryKey = 'uuid';\n\n    protected $table = 'web_crawled_urls';\n\n    protected $guarded = [];\n\n    protected $casts = [\n        'crawled' => 'bool',\n        'ignored' => 'bool',\n    ];\n\n    public function site(): BelongsTo\n    {\n        return $this->belongsTo(Site::class);\n    }\n\n    public function team(): BelongsTo\n    {\n        return $this->belongsTo(Team::class);\n    }\n\n    public function crawler(): BelongsTo\n    {\n        return $this->belongsTo(Crawler::class);\n    }\n\n    public function foundOn(): BelongsTo\n    {\n        return $this->belongsTo(static::class, 'found_on_id', 'uuid');\n    }\n\n    public function hash(): void\n    {\n        if ($this->url_hash === null) {\n            $this->url_hash = md5($this->url);\n        }\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Models/Crawler.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Crawler\\Enums\\State;\nuse Vigilant\\Crawler\\Observers\\CrawlerObserver;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Observers\\TeamObserver;\n\n/**\n * @property int $id\n * @property bool $enabled\n * @property ?int $site_id\n * @property int $team_id\n * @property State $state\n * @property string $schedule\n * @property string $start_url\n * @property ?array $sitemaps\n * @property ?array $crawler_stats\n * @property array $settings\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Site $site\n * @property ?Team $team\n * @property Collection<int, CrawledUrl> $urls\n */\n#[ObservedBy([TeamObserver::class, CrawlerObserver::class])]\n#[ScopedBy(TeamScope::class)]\nclass Crawler extends Model\n{\n    protected $table = 'web_crawlers';\n\n    protected $guarded = [];\n\n    protected $casts = [\n        'enabled' => 'boolean',\n        'state' => State::class,\n        'crawler_stats' => 'array',\n        'sitemaps' => 'array',\n        'settings' => 'array',\n    ];\n\n    public function totalUrlCount(): int\n    {\n        return $this->crawler_stats === null\n            ? $this->urls()->count()\n            : $this->crawler_stats['total_url_count'] ?? 0;\n    }\n\n    public function issueCount(): ?int\n    {\n        return $this->crawler_stats === null\n            ? null\n            : $this->crawler_stats['issue_count'] ?? 0;\n    }\n\n    public function site(): BelongsTo\n    {\n        return $this->belongsTo(Site::class);\n    }\n\n    public function team(): BelongsTo\n    {\n        return $this->belongsTo(Team::class);\n    }\n\n    public function urls(): HasMany\n    {\n        return $this->hasMany(CrawledUrl::class, 'crawler_id', 'id');\n    }\n\n    public function ignoredUrls(): HasMany\n    {\n        return $this->hasMany(IgnoredUrl::class, 'crawler_id', 'id');\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Models/IgnoredUrl.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Crawler\\Observers\\IgnoredUrlObserver;\n\n/**\n * @property int $id\n * @property int $crawler_id\n * @property string $url_hash\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property Crawler $crawler\n */\n#[ObservedBy(IgnoredUrlObserver::class)]\nclass IgnoredUrl extends Model\n{\n    protected $table = 'web_crawler_ignored_urls';\n\n    protected $guarded = [];\n\n    public function url(): HasOne\n    {\n        return $this->hasOne(CrawledUrl::class, 'url_hash', 'url_hash')\n            ->where('crawler_id', '=', $this->crawler_id);\n    }\n\n    public function crawler(): BelongsTo\n    {\n        return $this->belongsTo(Crawler::class);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Notifications/RatelimitedNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Notifications;\n\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass RatelimitedNotification extends Notification implements HasSite\n{\n    public static string $name = 'Crawler Ratelimited';\n\n    public Level $level = Level::Warning;\n\n    public static ?int $defaultCooldown = 60;\n\n    public function __construct(public Crawler $crawler) {}\n\n    public function title(): string\n    {\n        return __('Crawler failed due to ratelimits');\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when the crawler is blocked by rate limiting on the target site.');\n    }\n\n    public function viewUrl(): ?string\n    {\n        return route('crawler.view', ['crawler' => $this->crawler]);\n    }\n\n    public function site(): ?Site\n    {\n        return $this->crawler->site;\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->crawler->id;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Notifications/UrlIssuesNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Notifications;\n\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass UrlIssuesNotification extends Notification implements HasSite\n{\n    public static string $name = 'URLs with issues found';\n\n    public Level $level = Level::Warning;\n\n    public function __construct(public Crawler $crawler) {}\n\n    public function title(): string\n    {\n        /** @var int $count */\n        $count = $this->crawler->crawler_stats['issue_count'] ?? 0;\n\n        return __(':count URL\\'s found with issues on :site', [\n            'count' => $count,\n            'site' => $this->site()->url ?? $this->crawler->start_url,\n        ]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when the crawler discovers URLs with broken links, errors, or other issues.');\n    }\n\n    public function viewUrl(): ?string\n    {\n        return route('crawler.view', ['crawler' => $this->crawler]);\n    }\n\n    public function site(): ?Site\n    {\n        return $this->crawler->site;\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->crawler->id;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Observers/CrawledUrlObserver.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Observers;\n\nuse Vigilant\\Crawler\\Models\\CrawledUrl;\nuse Vigilant\\Crawler\\Models\\IgnoredUrl;\n\nclass CrawledUrlObserver\n{\n    public function creating(CrawledUrl $url): void\n    {\n        $url->hash();\n\n        $url->ignored = IgnoredUrl::query()\n            ->where('crawler_id', '=', $url->crawler_id)\n            ->where('url_hash', '=', $url->url_hash)\n            ->exists();\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Observers/CrawlerObserver.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Observers;\n\nuse Vigilant\\Crawler\\Jobs\\StartCrawlerJob;\nuse Vigilant\\Crawler\\Models\\Crawler;\n\nclass CrawlerObserver\n{\n    public function created(Crawler $crawler): void\n    {\n        StartCrawlerJob::dispatch($crawler);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Observers/IgnoredUrlObserver.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Observers;\n\nuse Vigilant\\Crawler\\Jobs\\CollectCrawlerStatsJob;\nuse Vigilant\\Crawler\\Models\\IgnoredUrl;\n\nclass IgnoredUrlObserver\n{\n    public function created(IgnoredUrl $url): void\n    {\n        CollectCrawlerStatsJob::dispatch($url->crawler, false);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler;\n\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\nuse Livewire\\Livewire;\nuse Vigilant\\Core\\Facades\\Navigation;\nuse Vigilant\\Core\\Policies\\AllowAllPolicy;\nuse Vigilant\\Crawler\\Commands\\CollectCrawlerStatsCommand;\nuse Vigilant\\Crawler\\Commands\\CrawlUrlsCommand;\nuse Vigilant\\Crawler\\Commands\\ProcessCrawlerStatesCommand;\nuse Vigilant\\Crawler\\Commands\\ScheduleCrawlersCommand;\nuse Vigilant\\Crawler\\Commands\\StartCrawlerCommand;\nuse Vigilant\\Crawler\\Events\\CrawlerFinishedEvent;\nuse Vigilant\\Crawler\\Listeners\\CrawlerFinishedListener;\nuse Vigilant\\Crawler\\Livewire\\Crawler\\Dashboard;\nuse Vigilant\\Crawler\\Livewire\\CrawlerForm;\nuse Vigilant\\Crawler\\Livewire\\Crawlers;\nuse Vigilant\\Crawler\\Livewire\\Tables\\CrawledUrlsTable;\nuse Vigilant\\Crawler\\Livewire\\Tables\\CrawlerTable;\nuse Vigilant\\Crawler\\Livewire\\Tables\\IssuesTable;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Crawler\\Notifications\\RatelimitedNotification;\nuse Vigilant\\Crawler\\Notifications\\UrlIssuesNotification;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Sites\\Conditions\\SiteCondition;\nuse Vigilant\\Users\\Models\\User;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function register(): void\n    {\n        $this\n            ->registerConfig();\n    }\n\n    protected function registerConfig(): static\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/crawler.php', 'crawler');\n\n        return $this;\n    }\n\n    public function boot(): void\n    {\n        $this\n            ->bootConfig()\n            ->bootMigrations()\n            ->bootCommands()\n            ->bootViews()\n            ->bootLivewire()\n            ->bootRoutes()\n            ->bootEvents()\n            ->bootNavigation()\n            ->bootNotifications()\n            ->bootGates()\n            ->bootPolicies();\n    }\n\n    protected function bootConfig(): static\n    {\n        $this->publishes([\n            __DIR__.'/../config/crawler.php' => config_path('crawler.php'),\n        ], 'config');\n\n        return $this;\n    }\n\n    protected function bootMigrations(): static\n    {\n        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');\n\n        return $this;\n    }\n\n    protected function bootCommands(): static\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n                StartCrawlerCommand::class,\n                CrawlUrlsCommand::class,\n                CollectCrawlerStatsCommand::class,\n                ProcessCrawlerStatesCommand::class,\n                ScheduleCrawlersCommand::class,\n            ]);\n        }\n\n        return $this;\n    }\n\n    protected function bootViews(): static\n    {\n        $this->loadViewsFrom(__DIR__.'/../resources/views', 'crawler');\n\n        return $this;\n    }\n\n    protected function bootLivewire(): static\n    {\n        Livewire::component('crawlers', Crawlers::class);\n        Livewire::component('crawler-table', CrawlerTable::class);\n        Livewire::component('crawler-form', CrawlerForm::class);\n\n        Livewire::component('crawler-dashboard', Dashboard::class);\n        Livewire::component('crawler-issues-table', IssuesTable::class);\n        Livewire::component('crawler-crawled-urls-table', CrawledUrlsTable::class);\n\n        return $this;\n    }\n\n    protected function bootRoutes(): static\n    {\n        if (! $this->app->routesAreCached()) {\n            Route::middleware(['web', 'auth'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/web.php'));\n        }\n\n        return $this;\n    }\n\n    protected function bootEvents(): static\n    {\n        Event::listen(CrawlerFinishedEvent::class, CrawlerFinishedListener::class);\n\n        return $this;\n    }\n\n    protected function bootNavigation(): static\n    {\n        Navigation::path(__DIR__.'/../resources/navigation.php');\n\n        return $this;\n    }\n\n    protected function bootNotifications(): static\n    {\n        NotificationRegistry::registerNotification([\n            UrlIssuesNotification::class,\n            RatelimitedNotification::class,\n        ]);\n\n        NotificationRegistry::registerCondition(UrlIssuesNotification::class, [\n            SiteCondition::class,\n        ]);\n\n        NotificationRegistry::registerCondition(RatelimitedNotification::class, [\n            SiteCondition::class,\n        ]);\n\n        return $this;\n    }\n\n    protected function bootGates(): static\n    {\n        Gate::define('use-crawler', function (User $user): bool {\n            return ce();\n        });\n\n        return $this;\n    }\n\n    protected function bootPolicies(): static\n    {\n        if (ce()) {\n            Gate::policy(Crawler::class, AllowAllPolicy::class);\n\n            Gate::define('create-crawled-url', function (?User $user, Crawler $crawler): bool {\n                return true;\n            });\n        }\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Validation/EqualDomainRule.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Validation;\n\nuse Closure;\nuse Illuminate\\Contracts\\Validation\\DataAwareRule;\nuse Illuminate\\Contracts\\Validation\\InvokableRule;\n\nclass EqualDomainRule implements DataAwareRule, InvokableRule\n{\n    protected array $data = [];\n\n    public function setData(array $data): static\n    {\n        $this->data = $data;\n\n        return $this;\n    }\n\n    public function __invoke(string $attribute, mixed $value, Closure $fail): void\n    {\n        $startUrl = $this->data['start_url'] ?? null;\n        $sitemaps = $this->data['sitemaps'] ?? [];\n\n        if ($startUrl && ! empty($sitemaps)) {\n            $startUrlDomain = parse_url($startUrl, PHP_URL_HOST);\n\n            foreach ($sitemaps as $sitemap) {\n                $sitemapDomain = parse_url($sitemap, PHP_URL_HOST);\n\n                if ($sitemapDomain !== $startUrlDomain) {\n                    $fail(__('The domains of the start URL and sitemaps must be the same domain'));\n\n                    return;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "packages/crawler/src/Validation/ValidRegexLines.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Validation;\n\nuse Closure;\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\n\nclass ValidRegexLines implements ValidationRule\n{\n    public function validate(string $attribute, mixed $value, Closure $fail): void\n    {\n        collect(explode(\"\\n\", (string) $value))\n            ->map(fn (string $line): string => trim($line))\n            ->filter()\n            ->each(function (string $line) use ($fail): void {\n                if (@preg_match($line, '') === false) {\n                    $fail(__('One or more URL blacklist patterns are not valid regular expressions.'));\n                }\n            });\n    }\n}\n"
  },
  {
    "path": "packages/crawler/testbench.yaml",
    "content": "providers:\n  - Vigilant\\Crawler\\ServiceProvider\n"
  },
  {
    "path": "packages/crawler/tests/Actions/CrawUrlTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Tests\\Actions;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Crawler\\Actions\\CrawlUrl;\nuse Vigilant\\Crawler\\Enums\\State;\nuse Vigilant\\Crawler\\Models\\CrawledUrl;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Crawler\\Notifications\\RatelimitedNotification;\nuse Vigilant\\Crawler\\Tests\\TestCase;\n\nclass CrawUrlTest extends TestCase\n{\n    #[Test]\n    public function it_crawls_url(): void\n    {\n        Http::fake([\n            'https://govigilant.io/url-1' => Http::response('<html>\n            <a href=\"/relative-url\"></a>\n            <a href=\"/trailing-url/#\"></a>\n            <a href=\"http://govigilant.io/unsecure-url\"></a>\n            <a href=\"#\"></a>\n            <a href=\"/\"></a>\n            <a href=\"tel:+123\"></a>\n            <a href=\"mailto:vincent@govigilant.io\"></a>\n           </html>'),\n        ])->preventStrayRequests();\n\n        /** @var Crawler $crawler */\n        $crawler = Crawler::query()->create([\n            'start_url' => 'vigilant',\n            'state' => State::Crawling,\n            'schedule' => '0 0 * * *',\n        ]);\n\n        /** @var CrawledUrl $crawledUrl */\n        $crawledUrl = $crawler->urls()->create([\n            'url' => 'https://govigilant.io/url-1',\n            'crawled' => false,\n        ]);\n\n        /** @var CrawlUrl $action */\n        $action = app(CrawlUrl::class);\n        $action->crawl($crawledUrl);\n\n        $crawledUrl->refresh();\n        $this->assertEquals(200, $crawledUrl->status);\n        $this->assertTrue($crawledUrl->crawled);\n\n        $foundUrls = $crawler->urls()\n            ->where('crawled', '=', false)\n            ->pluck('url')\n            ->toArray();\n\n        $discoveredUrls = array_values(array_filter(\n            $foundUrls,\n            fn (string $url): bool => $url !== $crawler->start_url,\n        ));\n\n        $this->assertEquals([\n            'https://govigilant.io/relative-url',\n            'https://govigilant.io/trailing-url',\n            'http://govigilant.io/unsecure-url',\n            'https://govigilant.io',\n        ], $discoveredUrls);\n    }\n\n    #[Test]\n    public function it_handles_malformed_html(): void\n    {\n        Http::fake([\n            'https://govigilant.io/url-1' => Http::response('<html>\n         <div><a hrefno123!#@!><<<><>\n           </htl'),\n        ])->preventStrayRequests();\n\n        /** @var Crawler $crawler */\n        $crawler = Crawler::query()->create([\n            'start_url' => 'vigilant',\n            'state' => State::Crawling,\n            'schedule' => '0 0 * * *',\n        ]);\n\n        /** @var CrawledUrl $crawledUrl */\n        $crawledUrl = $crawler->urls()->create([\n            'url' => 'https://govigilant.io/url-1',\n            'crawled' => false,\n        ]);\n\n        /** @var CrawlUrl $action */\n        $action = app(CrawlUrl::class);\n        $action->crawl($crawledUrl);\n\n        $crawledUrl->refresh();\n        $this->assertEquals(2, $crawler->urls()->count());\n        $this->assertTrue($crawledUrl->crawled);\n    }\n\n    #[Test]\n    public function it_handles_ratelimiting(): void\n    {\n        RatelimitedNotification::fake();\n        Http::fake([\n            'https://govigilant.io/url-1' => Http::response('', 429),\n        ])->preventStrayRequests();\n\n        /** @var Crawler $crawler */\n        $crawler = Crawler::query()->create([\n            'start_url' => 'vigilant',\n            'state' => State::Crawling,\n            'schedule' => '0 0 * * *',\n        ]);\n\n        /** @var CrawledUrl $crawledUrl */\n        $crawledUrl = $crawler->urls()->create([\n            'url' => 'https://govigilant.io/url-1',\n            'crawled' => false,\n        ]);\n\n        /** @var CrawlUrl $action */\n        $action = app(CrawlUrl::class);\n        $action->crawl($crawledUrl);\n\n        $crawler->refresh();\n        $this->assertEquals(State::Ratelimited, $crawler->state);\n        $this->assertTrue($crawledUrl->crawled);\n    }\n\n    #[Test]\n    public function it_does_not_insert_blacklisted_urls(): void\n    {\n        Http::fake([\n            'https://govigilant.io/url-1' => Http::response('<html>\n            <a href=\"/products/shoes\"></a>\n            <a href=\"/checkout/cart\"></a>\n            <a href=\"/customer/account/login\"></a>\n            <a href=\"/about-us\"></a>\n           </html>'),\n        ])->preventStrayRequests();\n\n        /** @var Crawler $crawler */\n        $crawler = Crawler::query()->create([\n            'start_url' => 'https://govigilant.io',\n            'state' => State::Crawling,\n            'schedule' => '0 0 * * *',\n            'settings' => [\n                'url_blacklist' => implode(\"\\n\", [\n                    '~^https?://[^/]+/checkout/~i',\n                    '~^https?://[^/]+/customer/account/login~i',\n                ]),\n            ],\n        ]);\n\n        /** @var CrawledUrl $crawledUrl */\n        $crawledUrl = $crawler->urls()->create([\n            'url' => 'https://govigilant.io/url-1',\n            'crawled' => false,\n        ]);\n\n        /** @var CrawlUrl $action */\n        $action = app(CrawlUrl::class);\n        $action->crawl($crawledUrl);\n\n        $discoveredUrls = $crawler->urls()\n            ->where('crawled', '=', false)\n            ->pluck('url')\n            ->toArray();\n\n        $this->assertContains('https://govigilant.io/products/shoes', $discoveredUrls);\n        $this->assertContains('https://govigilant.io/about-us', $discoveredUrls);\n        $this->assertNotContains('https://govigilant.io/checkout/cart', $discoveredUrls);\n        $this->assertNotContains('https://govigilant.io/customer/account/login', $discoveredUrls);\n    }\n\n    #[Test]\n    public function it_inserts_all_urls_when_blacklist_is_empty(): void\n    {\n        Http::fake([\n            'https://govigilant.io/url-1' => Http::response('<html>\n            <a href=\"/checkout/cart\"></a>\n            <a href=\"/about-us\"></a>\n           </html>'),\n        ])->preventStrayRequests();\n\n        /** @var Crawler $crawler */\n        $crawler = Crawler::query()->create([\n            'start_url' => 'https://govigilant.io',\n            'state' => State::Crawling,\n            'schedule' => '0 0 * * *',\n        ]);\n\n        /** @var CrawledUrl $crawledUrl */\n        $crawledUrl = $crawler->urls()->create([\n            'url' => 'https://govigilant.io/url-1',\n            'crawled' => false,\n        ]);\n\n        /** @var CrawlUrl $action */\n        $action = app(CrawlUrl::class);\n        $action->crawl($crawledUrl);\n\n        $discoveredUrls = $crawler->urls()\n            ->where('crawled', '=', false)\n            ->pluck('url')\n            ->toArray();\n\n        $this->assertContains('https://govigilant.io/checkout/cart', $discoveredUrls);\n        $this->assertContains('https://govigilant.io/about-us', $discoveredUrls);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/tests/Actions/ProcessCrawlerStateTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Tests\\Actions;\n\nuse Illuminate\\Support\\Facades\\Event;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Crawler\\Actions\\ProcessCrawlerState;\nuse Vigilant\\Crawler\\Enums\\State;\nuse Vigilant\\Crawler\\Events\\CrawlerFinishedEvent;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Crawler\\Tests\\TestCase;\n\nclass ProcessCrawlerStateTest extends TestCase\n{\n    #[Test]\n    public function it_processes_crawling_finished_state(): void\n    {\n        Event::fake([CrawlerFinishedEvent::class]);\n\n        /** @var Crawler $crawler */\n        $crawler = Crawler::query()->create([\n            'start_url' => 'vigilant',\n            'state' => State::Crawling,\n            'schedule' => '0 0 * * *',\n        ]);\n\n        $crawler->urls()\n            ->where('url', '=', $crawler->start_url)\n            ->update(['crawled' => true]);\n\n        $crawler->urls()->create([\n            'url' => 'vigilant/url-1',\n            'crawled' => true,\n        ]);\n\n        /** @var ProcessCrawlerState $action */\n        $action = app(ProcessCrawlerState::class);\n        $action->process($crawler);\n\n        $crawler->refresh();\n\n        $this->assertEquals(State::Finished, $crawler->state);\n        Event::assertDispatched(CrawlerFinishedEvent::class);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/tests/Actions/StartCrawlerTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Tests\\Actions;\n\nuse Illuminate\\Support\\Facades\\Bus;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Crawler\\Actions\\StartCrawler;\nuse Vigilant\\Crawler\\Enums\\State;\nuse Vigilant\\Crawler\\Jobs\\ImportSitemapsJob;\nuse Vigilant\\Crawler\\Models\\CrawledUrl;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Crawler\\Tests\\TestCase;\n\nclass StartCrawlerTest extends TestCase\n{\n    #[Test]\n    public function it_starts_crawler(): void\n    {\n        /** @var Crawler $crawler */\n        $crawler = Crawler::query()->create([\n            'start_url' => 'vigilant',\n            'schedule' => '0 0 * * *',\n        ]);\n\n        /** @var StartCrawler $action */\n        $action = app(StartCrawler::class);\n        $action->start($crawler);\n\n        /** @var ?CrawledUrl $startUrl */\n        $startUrl = $crawler->urls()->firstWhere('url', '=', 'vigilant');\n\n        $this->assertNotNull($startUrl);\n        $crawler->refresh();\n        $this->assertEquals(State::Crawling, $crawler->state);\n        $this->assertNull($crawler->crawler_stats);\n    }\n\n    #[Test]\n    public function it_starts_sitemap_job(): void\n    {\n        Bus::fake();\n\n        /** @var Crawler $crawler */\n        $crawler = Crawler::query()->create([\n            'start_url' => 'vigilant',\n            'sitemaps' => ['sitemap-1'],\n            'schedule' => '0 0 * * *',\n        ]);\n\n        /** @var StartCrawler $action */\n        $action = app(StartCrawler::class);\n        $action->start($crawler);\n\n        Bus::assertDispatched(ImportSitemapsJob::class);\n    }\n}\n"
  },
  {
    "path": "packages/crawler/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\Crawler\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Livewire\\LivewireServiceProvider;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Crawler\\ServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        TeamService::fake();\n    }\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n            \\Vigilant\\Users\\ServiceProvider::class,\n            LivewireServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/cve/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/cve/composer.json",
    "content": "{\n    \"name\": \"vigilant/cve\",\n    \"description\": \"Vigilant CVE Monitor\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"guzzlehttp/guzzle\": \"^7.8\",\n        \"laravel/framework\": \"^12.0\",\n        \"livewire/livewire\": \"^3.4\",\n        \"vigilant/core\": \"@dev\",\n        \"vigilant/sites\": \"@dev\",\n        \"vigilant/users\": \"@dev\",\n        \"vigilant/frontend\": \"@dev\",\n        \"vigilant/notifications\": \"@dev\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Cve\\\\\": \"src\",\n            \"Vigilant\\\\Cve\\\\Database\\\\Factories\\\\\": \"database/factories\",\n            \"Vigilant\\\\Users\\\\Database\\\\Factories\\\\\": \"../users/database/factories\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Cve\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\Cve\\\\ServiceProvider\"\n            ]\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/cve/config/cve.php",
    "content": "<?php\n\nreturn [\n    'queue' => 'cve',\n];\n"
  },
  {
    "path": "packages/cve/database/migrations/2025_04_18_090000_create_cves_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('cves', function (Blueprint $table): void {\n            $table->id();\n\n            $table->string('identifier');\n            $table->float('score')->nullable();\n            $table->text('description');\n\n            $table->dateTime('published_at');\n            $table->dateTime('modified_at');\n\n            $table->json('data');\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('cves');\n    }\n};\n"
  },
  {
    "path": "packages/cve/database/migrations/2025_04_18_100000_create_cve_monitors_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('cve_monitors', function (Blueprint $table): void {\n            $table->id();\n            $table->foreignIdFor(Site::class)->nullable()->constrained()->onDelete('cascade');\n            $table->foreignIdFor(Team::class)->constrained()->onDelete('cascade');\n\n            $table->boolean('enabled')->default(true);\n            $table->string('keyword');\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('cve_monitors');\n    }\n};\n"
  },
  {
    "path": "packages/cve/database/migrations/2025_04_18_103000_create_cve_monitor_matches_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Models\\CveMonitor;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('cve_monitor_matches', function (Blueprint $table): void {\n            $table->id();\n\n            $table->foreignIdFor(Cve::class)->constrained()->onDelete('cascade');\n            $table->foreignIdFor(CveMonitor::class)->constrained()->onDelete('cascade');\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('cve_monitor_matches');\n    }\n};\n"
  },
  {
    "path": "packages/cve/database/migrations/2025_10_04_135717_add_fulltext_index_to_cves_description.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        // Only create FULLTEXT index for MySQL/MariaDB\n        // SQLite doesn't support FULLTEXT indexes\n        $driver = DB::connection()->getDriverName();\n\n        if (in_array($driver, ['mysql', 'mariadb'])) {\n            DB::statement('CREATE FULLTEXT INDEX idx_cves_description_fulltext ON cves(description)');\n        }\n    }\n\n    public function down(): void\n    {\n        $driver = DB::connection()->getDriverName();\n\n        if (in_array($driver, ['mysql', 'mariadb'])) {\n            DB::statement('DROP INDEX idx_cves_description_fulltext ON cves');\n        }\n    }\n};\n"
  },
  {
    "path": "packages/cve/database/migrations/2025_10_04_135739_add_unique_index_to_cve_monitor_matches.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('cve_monitor_matches', function (Blueprint $table): void {\n            $table->unique(['cve_id', 'cve_monitor_id'], 'unique_cve_monitor_match');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('cve_monitor_matches', function (Blueprint $table): void {\n            $table->dropUnique('unique_cve_monitor_match');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/cve/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n    ignoreErrors:\n        - identifier: missingType.iterableValue\n        - identifier: missingType.generics\n"
  },
  {
    "path": "packages/cve/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/cve/resources/navigation.php",
    "content": "<?php\n\nuse Vigilant\\Core\\Facades\\Navigation;\n\nNavigation::add(route('cve.index'), 'CVEs')\n    ->parent('infrastructure')\n    ->icon('phosphor-shield-star')\n    ->gate('use-cve')\n    ->routeIs('cve*')\n    ->sort(700);\n"
  },
  {
    "path": "packages/cve/resources/views/components/empty-states/monitors.blade.php",
    "content": "<x-frontend::empty-state\n    :title=\"__('No CVE Monitors')\"\n    :description=\"__('Add a CVE monitor to watch for newly disclosed vulnerabilities that match your keywords and severity thresholds.')\"\n    icon=\"phosphor-warning-circle\"\n    iconClass=\"h-12 w-12 text-rose\"\n    iconWrapperClass=\"rounded-full bg-rose/10 p-4 mb-6\"\n    :buttonHref=\"route('cve.monitor.create')\"\n    :buttonText=\"__('Add CVE Monitor')\"\n    buttonClass=\"bg-rose hover:bg-rose/90 text-base-50 px-5 py-2.5 rounded-lg transition-all duration-300\"\n/>\n"
  },
  {
    "path": "packages/cve/resources/views/cve.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header :back=\"route('cve.monitor.view', ['monitor' => $monitor])\" :title=\"$cve->identifier\" />\n    </x-slot>\n\n    <div class=\"space-y-4\">\n        <dl class=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n            <x-frontend::stats-card :title=\"__('Identifier')\">\n                {{ $cve->identifier }}\n            </x-frontend::stats-card>\n\n            <x-frontend::stats-card :title=\"__('Score')\">\n                {{ $cve->score ?? 0 }}\n            </x-frontend::stats-card>\n\n            <x-frontend::stats-card :title=\"__('Published At')\">\n                {{ $cve->published_at->format('Y-m-d H:i:s') }}\n            </x-frontend::stats-card>\n\n            <x-frontend::stats-card :title=\"__('Last Modified')\">\n                {{ $cve->modified_at->format('Y-m-d H:i:s') }}\n            </x-frontend::stats-card>\n        </dl>\n\n        <x-frontend::card>\n            <div class=\"text-left\">\n                <p class=\"text-left text-base-100 leading-relaxed\">{{ $cve->description }}</p>\n\n                <h3 class=\"text-lg font-bold mt-4 text-base-50\">@lang('References')</h3>\n                <ul class=\"list-disc pl-5 text-base-200 space-y-1\">\n                    @foreach (data_get($cve->data, 'cve.references.reference_data', []) as $reference)\n                        <li>\n                            <a class=\"text-red hover:text-red-light underline transition-colors duration-200\" href=\"{{ $reference['url'] }}\" target=\"_blank\">\n                                {{ $reference['name'] ?? $reference['url'] }}\n                            </a>\n                        </li>\n                    @endforeach\n                </ul>\n            </div>\n        </x-frontend::card>\n    </div>\n</x-app-layout>\n"
  },
  {
    "path": "packages/cve/resources/views/index.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header title=\"CVE Monitoring\">\n            <x-frontend::page-header.actions>\n                <x-create-button dusk=\"cve-add-button\" :href=\"route('cve.monitor.create')\" model=\"Vigilant\\Cve\\Models\\CveMonitor\">\n                    @lang('Add CVE Monitor')\n                </x-create-button>\n            </x-frontend::page-header.actions>\n            <x-frontend::page-header.mobile-actions>\n                <x-create-button-dropdown :href=\"route('cve.monitor.create')\" model=\"Vigilant\\Cve\\Models\\CveMonitor\">\n                    @lang('Add CVE Monitor')\n                </x-create-button-dropdown>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    @if ($hasMonitors)\n        <livewire:cve-monitor-table />\n    @else\n        <x-cve::empty-states.monitors />\n    @endif\n</x-app-layout>\n"
  },
  {
    "path": "packages/cve/resources/views/livewire/cve-monitor-form.blade.php",
    "content": "<div>\n    <x-slot name=\"header\">\n        <x-page-header :title=\"$updating\n            ? __('Edit CVE Monitor - :keyword', ['keyword' => $cveMonitor->keyword])\n            : __('Add CVE Monitor')\" :back=\"route('cve.index')\">\n        </x-page-header>\n    </x-slot>\n\n    <form wire:submit.prevent=\"save\">\n        <div class=\"max-w-7xl mx-auto\">\n            <x-card>\n                <div class=\"flex flex-col gap-4\">\n                    @if (!$inline)\n                        <x-form.checkbox field=\"form.enabled\" name=\"Enabled\" description=\"Enable or disable this CVE monitor\" />\n                    @endif\n\n                    <x-form.text field=\"form.keyword\" name=\"Keyword\"\n                        description=\"Keyword to monitor. Enter your platform or software here, for example: Wordpress. This is not case-sensitive.\" />\n\n                    <div class=\"flex justify-end gap-4 items-center\">\n                        <x-form.submit-button dusk=\"submit-button\" wire:loading.attr=\"disabled\" :submitText=\"$updating ? 'Save' : 'Create'\" />\n                    </div>\n                </div>\n            </x-card>\n        </div>\n    </form>\n</div>\n"
  },
  {
    "path": "packages/cve/resources/views/monitor.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header :back=\"route('cve.index')\" :title=\"'CVE Monitor - ' . $monitor->keyword . (!$monitor->enabled ? ' (Disabled)' : '')\">\n            <x-frontend::page-header.actions>\n                <x-form.button dusk=\"monitor-edit-button\" :href=\"route('cve.monitor.edit', ['monitor' => $monitor])\">\n                    @lang('Edit')\n                </x-form.button>\n                <x-form.button class=\"bg-red\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.button>\n            </x-frontend::page-header.actions>\n            \n            <x-frontend::page-header.mobile-actions>\n                <x-form.dropdown-button :href=\"route('cve.monitor.edit', ['monitor' => $monitor])\">\n                    @lang('Edit')\n                </x-form.dropdown-button>\n                <x-form.dropdown-button class=\"!text-red hover:!text-red-light\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.dropdown-button>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    <div>\n        <h2 class=\"text-xl font-bold leading-7 sm:truncate sm:text-2xl sm:tracking-tight text-neutral-100 mb-2\">\n            {{ __('Matched CVE\\'s') }}\n        </h2>\n\n        <livewire:cve-monitor-matches-table :monitor=\"$monitor\" wire:key=\"matches-table\" />\n    </div>\n\n    <!-- Delete Confirmation Modal -->\n    <div x-data=\"{ showDeleteModal: false }\" @open-delete-modal.window=\"showDeleteModal = true\">\n        <x-frontend::modal show=\"showDeleteModal\">\n            <x-frontend::modal.header icon=\"phosphor-trash\" iconColor=\"red\" show=\"showDeleteModal\">\n                @lang('Delete CVE Monitor')\n            </x-frontend::modal.header>\n\n            <x-frontend::modal.body>\n                <div class=\"space-y-4\">\n                    <p class=\"text-base-100\">\n                        @lang('Are you sure you want to delete this CVE monitor?')\n                    </p>\n                    <div class=\"bg-base-850 border border-base-700 rounded-lg p-4\">\n                        <div class=\"flex items-start gap-3\">\n                            <div class=\"flex-shrink-0\">\n                                @svg('phosphor-warning-circle', 'w-5 h-5 text-orange mt-0.5')\n                            </div>\n                            <div class=\"flex-1\">\n                                <p class=\"text-sm text-base-300\">\n                                    Keyword: <span class=\"font-semibold text-base-100\">{{ $monitor->keyword }}</span>\n                                </p>\n                                <p class=\"text-sm text-base-400 mt-1\">\n                                    @lang('This action cannot be undone. All matched CVEs for this monitor will be permanently deleted.')\n                                </p>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </x-frontend::modal.body>\n\n            <x-frontend::modal.footer>\n                <x-form.button type=\"button\" @click=\"showDeleteModal = false\">\n                    @lang('Cancel')\n                </x-form.button>\n                <form action=\"{{ route('cve.monitor.delete', ['monitor' => $monitor]) }}\" method=\"POST\" class=\"inline\">\n                    @csrf\n                    @method('DELETE')\n                    <x-form.button class=\"bg-red\" type=\"submit\">\n                        @lang('Delete Monitor')\n                    </x-form.button>\n                </form>\n            </x-frontend::modal.footer>\n        </x-frontend::modal>\n    </div>\n\n</x-app-layout>\n"
  },
  {
    "path": "packages/cve/routes/web.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\Cve\\Http\\Controllers\\CveController;\nuse Vigilant\\Cve\\Http\\Controllers\\CveMonitorController;\nuse Vigilant\\Cve\\Livewire\\CveMonitorForm;\n\nRoute::prefix('cve')\n    ->group(function () {\n        Route::get('/', [CveMonitorController::class, 'list'])->name('cve.index');\n        Route::get('/create', CveMonitorForm::class)->name('cve.monitor.create');\n        Route::get('/edit/{monitor}', CveMonitorForm::class)->name('cve.monitor.edit');\n        Route::get('/{monitor}', [CveMonitorController::class, 'view'])->name('cve.monitor.view');\n        Route::delete('/{monitor}', [CveMonitorController::class, 'delete'])->name('cve.monitor.delete')->can('delete,monitor');\n\n        Route::get('/{monitor}/{cve}', [CveController::class, 'view'])->name('cve.view');\n    });\n"
  },
  {
    "path": "packages/cve/src/Actions/ImportAllCves.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Actions;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Cve\\Jobs\\ImportAllCvesJob;\n\nclass ImportAllCves\n{\n    public function __construct(\n        protected ImportCve $importCve,\n    ) {}\n\n    public function import(int $page): void\n    {\n        $endpoint = 'https://services.nvd.nist.gov/rest/json/cves/2.0';\n\n        $pageSize = 500;\n\n        $response = Http::get($endpoint, [\n            'resultsPerPage' => $pageSize,\n            'startIndex' => $page * $pageSize,\n        ])->throw();\n\n        $cves = $response->json('vulnerabilities', []);\n\n        foreach ($cves as $cve) {\n            $this->importCve->import($cve, false);\n        }\n\n        if (count($cves) === $pageSize) {\n            ImportAllCvesJob::dispatch($page + 1)->delay(now()->addSeconds(30));\n        }\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Actions/ImportCve.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Actions;\n\nuse Illuminate\\Support\\Arr;\nuse Vigilant\\Cve\\Jobs\\MatchCveMonitorsJob;\nuse Vigilant\\Cve\\Models\\Cve;\n\nclass ImportCve\n{\n    public function import(array $cve, bool $notify = true): void\n    {\n        /** @var array<int, array<string, string>> $descriptions */\n        $descriptions = data_get($cve, 'cve.descriptions', []);\n\n        $description = collect($descriptions)->firstWhere('lang', '=', 'en');\n\n        if ($description === null) {\n            $description = Arr::first($descriptions);\n        }\n\n        $score = data_get($cve, 'cve.metrics.cvssMetricV31.0.cvssData.baseScore');\n\n        if ($score === null) {\n            $score = data_get($cve, 'cve.metrics.cvssMetricV2.0.cvssData.baseScore');\n        }\n\n        $model = Cve::query()->updateOrCreate([\n            'identifier' => data_get($cve, 'cve.id'),\n        ], [\n            'score' => $score,\n            'description' => $description['value'] ?? '',\n            'published_at' => data_get($cve, 'cve.published'),\n            'modified_at' => data_get($cve, 'cve.lastModified'),\n            'data' => $cve,\n        ]);\n\n        if ($notify && $model->wasRecentlyCreated && $model->published_at->isAfter(now()->subWeek())) {\n            MatchCveMonitorsJob::dispatch($model);\n        }\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Actions/ImportCves.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Actions;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Cve\\Models\\Cve;\n\nclass ImportCves\n{\n    public function __construct(\n        protected ImportCve $importCve,\n    ) {}\n\n    public function import(?Carbon $from = null): void\n    {\n        if ($from === null) {\n            $from = Cve::query()\n                ->orderBy('published_at', 'desc')\n                ->first()->published_at ?? null;\n        }\n        if ($from === null) {\n            $from = now()->subDay();\n        }\n\n        $endpoint = 'https://services.nvd.nist.gov/rest/json/cves/2.0';\n\n        $to = $from->clone();\n        $to->addDays(30);\n\n        if ($to->isFuture()) {\n            $to = now();\n        }\n\n        $response = Http::get($endpoint, [\n            'pubStartDate' => $from->format('Y-m-d\\TH:i:s\\Z'),\n            'pubEndDate' => $to->format('Y-m-d\\TH:i:s\\Z'),\n        ])->throw();\n\n        $cves = $response->json('vulnerabilities', []);\n\n        foreach ($cves as $cve) {\n            $this->importCve->import($cve);\n        }\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Actions/MatchCve.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Actions;\n\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Models\\CveMonitor;\nuse Vigilant\\Cve\\Notifications\\CveMatchedNotification;\n\nclass MatchCve\n{\n    public function match(CveMonitor $monitor, Cve $cve): void\n    {\n        $matches = str($cve->description)->lower()->contains(strtolower($monitor->keyword));\n\n        if (! $matches) {\n            return;\n        }\n\n        $monitor->matches()->firstOrCreate([\n            'cve_id' => $cve->id,\n        ]);\n\n        CveMatchedNotification::notify($monitor, $cve);\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Actions/MatchExistingCves.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Actions;\n\nuse Illuminate\\Support\\Facades\\DB;\nuse Vigilant\\Cve\\Models\\CveMonitor;\n\nclass MatchExistingCves\n{\n    public function match(CveMonitor $monitor): void\n    {\n        // Skip SQLite-incompatible query when running tests\n        if (app()->runningUnitTests()) {\n            return;\n        }\n\n        DB::statement('\n                INSERT IGNORE INTO cve_monitor_matches (cve_id, cve_monitor_id, created_at, updated_at)\n                SELECT\n                    id as cve_id,\n                    ? as cve_monitor_id,\n                    NOW() as created_at,\n                    NOW() as updated_at\n                FROM cves\n                WHERE MATCH(description) AGAINST(? IN BOOLEAN MODE)\n            ', [\n            $monitor->id,\n            $monitor->keyword,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Commands/ImportAllCvesCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Cve\\Jobs\\ImportAllCvesJob;\n\nclass ImportAllCvesCommand extends Command\n{\n    protected $signature = 'cve:import-all {page=0}';\n\n    protected $description = 'Import all CVEs';\n\n    public function handle(): int\n    {\n        /** @var int $page */\n        $page = $this->argument('page');\n\n        ImportAllCvesJob::dispatch($page);\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Commands/ImportCvesCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Cve\\Jobs\\ImportCvesJob;\n\nclass ImportCvesCommand extends Command\n{\n    protected $signature = 'cve:import {from?}';\n\n    protected $description = 'Import new CVEs';\n\n    public function handle(): int\n    {\n        /** @var ?string $from */\n        $from = $this->argument('from');\n\n        $from = $from !== null ? Carbon::parse($from) : null;\n\n        ImportCvesJob::dispatch($from);\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Commands/MatchCveCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Cve\\Jobs\\MatchCveJob;\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Models\\CveMonitor;\n\nclass MatchCveCommand extends Command\n{\n    protected $signature = 'cve:match {monitorId} {cveId}';\n\n    protected $description = 'Match CVE to Monitor';\n\n    public function handle(): int\n    {\n        /** @var ?int $monitorId */\n        $monitorId = $this->argument('monitorId');\n\n        /** @var ?int $cveId */\n        $cveId = $this->argument('cveId');\n\n        $monitor = CveMonitor::query()\n            ->withoutGlobalScopes()\n            ->findOrFail($monitorId);\n\n        $cve = Cve::query()->findOrFail($cveId);\n\n        MatchCveJob::dispatch($monitor, $cve);\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Commands/MatchExistingCvesCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Cve\\Jobs\\MatchExistingCvesJob;\nuse Vigilant\\Cve\\Models\\CveMonitor;\n\nclass MatchExistingCvesCommand extends Command\n{\n    protected $signature = 'cve:match-existing {monitorId?}';\n\n    protected $description = 'Match existing CVEs';\n\n    public function handle(): int\n    {\n        /** @var ?int $monitorId */\n        $monitorId = $this->argument('monitorId');\n\n        $monitors = CveMonitor::query()\n            ->withoutGlobalScopes()\n            ->when($monitorId, function ($query) use ($monitorId) {\n                return $query->where('id', $monitorId);\n            })\n            ->get();\n\n        foreach ($monitors as $monitor) {\n            MatchExistingCvesJob::dispatch($monitor);\n        }\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Http/Controllers/CveController.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Http\\Controllers;\n\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\View\\View;\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Models\\CveMonitor;\n\nclass CveController extends Controller\n{\n    public function view(CveMonitor $monitor, Cve $cve): View\n    {\n        /** @var view-string $view */\n        $view = 'cve::cve';\n\n        return view($view, [\n            'monitor' => $monitor,\n            'cve' => $cve,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Http/Controllers/CveMonitorController.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Http\\Controllers;\n\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\View\\View;\nuse Vigilant\\Cve\\Models\\CveMonitor;\n\nclass CveMonitorController extends Controller\n{\n    public function list(): View\n    {\n        /** @var view-string $view */\n        $view = 'cve::index';\n        $hasMonitors = CveMonitor::query()->exists();\n\n        return view($view, [\n            'hasMonitors' => $hasMonitors,\n        ]);\n    }\n\n    public function view(CveMonitor $monitor): View\n    {\n        /** @var view-string $view */\n        $view = 'cve::monitor';\n\n        return view($view, [\n            'monitor' => $monitor,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Jobs/ImportAllCvesJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Cve\\Actions\\ImportAllCves;\n\nclass ImportAllCvesJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public int $tries = 3;\n\n    public int $backoff = 30;\n\n    public function __construct(protected int $page = 0)\n    {\n        $this->onQueue(config()->string('cve.queue'));\n    }\n\n    public function handle(ImportAllCves $importer): void\n    {\n        $importer->import($this->page);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->page;\n    }\n\n    public function tags(): array\n    {\n        return [\n            $this->page,\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Jobs/ImportCvesJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Cve\\Actions\\ImportCves;\n\nclass ImportCvesJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public int $tries = 3;\n\n    public int $backoff = 30;\n\n    public function __construct(protected ?Carbon $from = null)\n    {\n        $this->onQueue(config()->string('cve.queue'));\n    }\n\n    public function handle(ImportCves $importer): void\n    {\n        $importer->import($this->from);\n    }\n\n    public function uniqueId(): string\n    {\n        return $this->from?->format('Y-m-d') ?? 'recent';\n    }\n\n    public function tags(): array\n    {\n        return [\n            $this->from?->format('Y-m-d') ?? 'recent',\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Jobs/MatchCveJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Cve\\Actions\\MatchCve;\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Models\\CveMonitor;\n\nclass MatchCveJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(\n        protected CveMonitor $monitor,\n        protected Cve $cve\n    ) {\n        $this->onQueue(config()->string('cve.queue'));\n    }\n\n    public function handle(MatchCve $matcher, TeamService $teamService): void\n    {\n        $teamService->setTeamById($this->monitor->team_id);\n        $matcher->match($this->monitor, $this->cve);\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Jobs/MatchCveMonitorsJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Models\\CveMonitor;\n\nclass MatchCveMonitorsJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(\n        protected Cve $cve\n    ) {\n        $this->onQueue(config()->string('cve.queue'));\n    }\n\n    public function handle(): void\n    {\n        CveMonitor::query()\n            ->withoutGlobalScopes()\n            ->each(function (CveMonitor $monitor): void {\n                MatchCveJob::dispatch($monitor, $this->cve);\n            });\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->cve->id;\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Jobs/MatchExistingCvesJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Cve\\Actions\\MatchExistingCves;\nuse Vigilant\\Cve\\Models\\CveMonitor;\n\nclass MatchExistingCvesJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(\n        protected CveMonitor $monitor\n    ) {\n        $this->onQueue(config()->string('cve.queue'));\n    }\n\n    public function handle(MatchExistingCves $matcher, TeamService $teamService): void\n    {\n        $teamService->setTeamById($this->monitor->team_id);\n        $matcher->match($this->monitor);\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Livewire/CveMonitorForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Livewire;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\nuse Vigilant\\Cve\\Models\\CveMonitor;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Frontend\\Traits\\CanBeInline;\n\nclass CveMonitorForm extends Component\n{\n    use CanBeInline;\n    use DisplaysAlerts;\n\n    public Forms\\CveMonitorForm $form;\n\n    #[Locked]\n    public CveMonitor $cveMonitor;\n\n    public function mount(?CveMonitor $monitor): void\n    {\n        if ($monitor !== null) {\n            if ($monitor->exists) {\n                $this->authorize('update', $monitor);\n            } else {\n                $this->authorize('create', $monitor);\n            }\n\n            $this->form->fill($monitor->toArray());\n            $this->cveMonitor = $monitor;\n        }\n    }\n\n    public function save(): void\n    {\n        $this->validate();\n\n        if ($this->cveMonitor->exists) {\n            $this->authorize('update', $this->cveMonitor);\n\n            $this->cveMonitor->update($this->form->all());\n        } else {\n            $this->authorize('create', $this->cveMonitor);\n\n            $this->cveMonitor = CveMonitor::query()->create(\n                $this->form->all()\n            );\n        }\n\n        $this->alert(\n            __('Saved'),\n            __('CVE monitor was successfully :action',\n                ['action' => $this->cveMonitor->wasRecentlyCreated ? 'created' : 'saved']),\n            AlertType::Success\n        );\n        $this->redirectRoute('cve.index');\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'cve::livewire.cve-monitor-form';\n\n        return view($view, [\n            'updating' => $this->cveMonitor->exists,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Livewire/Forms/CveMonitorForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Form;\nuse Vigilant\\Core\\Validation\\CanEnableRule;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\n\nclass CveMonitorForm extends Form\n{\n    #[Locked]\n    public ?int $site_id;\n\n    public bool $enabled = true;\n\n    public string $keyword = '';\n\n    public function rules(): array\n    {\n        return [\n            'keyword' => ['required', 'max:255'],\n            'enabled' => ['boolean', new CanEnableRule(DnsMonitor::class)],\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Livewire/Tables/CveMonitorMatchesTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Livewire\\Attributes\\Locked;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse Vigilant\\Cve\\Models\\CveMonitor;\nuse Vigilant\\Cve\\Models\\CveMonitorMatch;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\n\nclass CveMonitorMatchesTable extends BaseTable\n{\n    protected string $model = CveMonitorMatch::class;\n\n    #[Locked]\n    public CveMonitor $monitor;\n\n    public function mount(CveMonitor $monitor): void\n    {\n        $this->monitor = $monitor;\n    }\n\n    protected function columns(): array\n    {\n        return [\n            Column::make(__('CVE'), 'cve.identifier')\n                ->searchable()\n                ->sortable(),\n\n            Column::make(__('Score'), 'cve.score')\n                ->searchable()\n                ->sortable(),\n\n            Column::make(__('Description'), 'cve.description')\n                ->displayUsing(fn (string $description): string => str($description)->limit(100))\n                ->searchable()\n                ->sortable(),\n        ];\n    }\n\n    protected function link(Model $record): string\n    {\n        /** @var CveMonitorMatch $record */\n        return route('cve.view', ['monitor' => $record->cveMonitor, 'cve' => $record->cve]);\n    }\n\n    protected function query(): Builder\n    {\n        return parent::query()\n            ->where('cve_monitor_id', '=', $this->monitor->id)\n            ->with('cve');\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Livewire/Tables/CveMonitorTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Query\\Builder as Query;\nuse Illuminate\\Support\\Enumerable;\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Gate;\nuse RamonRietdijk\\LivewireTables\\Actions\\Action;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse RamonRietdijk\\LivewireTables\\Enums\\Direction;\nuse RamonRietdijk\\LivewireTables\\Filters\\SelectFilter;\nuse Vigilant\\Cve\\Actions\\ImportAllCves;\nuse Vigilant\\Cve\\Models\\CveMonitor;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\Enums\\Status;\nuse Vigilant\\Frontend\\Integrations\\Table\\StatusColumn;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass CveMonitorTable extends BaseTable\n{\n    protected string $model = CveMonitor::class;\n\n    protected function columns(): array\n    {\n        return [\n            StatusColumn::make(__('Status'))\n                ->text(function (CveMonitor $monitor): string {\n                    return $monitor->enabled ? __('Enabled') : __('Disabled');\n                })\n                ->status(function (CveMonitor $monitor): Status {\n                    return $monitor->enabled ? Status::Success : Status::Danger;\n                }),\n\n            Column::make(__('Keyword'), 'keyword')\n                ->searchable()\n                ->sortable(),\n\n            Column::make(__('Matched CVE\\'s'), 'total_matches')\n                ->sortable(function (Builder $builder, Direction $direction): void {\n                    $builder->orderBy(function (Query $query): void {\n                        $query->selectRaw('COUNT(*)')\n                            ->from('cve_monitor_matches')\n                            ->whereColumn('cve_monitor_matches.cve_monitor_id', 'cve_monitors.id');\n                    }, $direction->value);\n                })->searchable(function (Builder $builder, mixed $value): void {\n                    $builder->where(function (Builder $query): void {\n                        $query->selectRaw('COUNT(*)')\n                            ->from('cve_monitor_matches')\n                            ->whereColumn('cve_monitor_matches.cve_monitor_id', 'cve_monitors.id');\n                    }, '=', $value);\n                }),\n        ];\n    }\n\n    protected function link(Model $record): string\n    {\n        return route('cve.monitor.view', ['monitor' => $record]);\n    }\n\n    protected function filters(): array\n    {\n        return [\n            SelectFilter::make(__('Site'), 'site_id')\n                ->options(\n                    Site::query()\n                        ->orderBy('url')\n                        ->pluck('url', 'id')\n                        ->toArray()\n                ),\n        ];\n    }\n\n    protected function actions(): array\n    {\n        $actions = [\n            Action::make(__('Enable'), function (Enumerable $models): void {\n                foreach ($models as $model) {\n                    if (! Gate::allows('create', $model)) {\n                        break;\n                    }\n\n                    $model->update(['enabled' => true]);\n                }\n            }, 'enable'),\n\n            Action::make(__('Disable'), function (Enumerable $models): void {\n                $models->each(fn (CveMonitor $monitor) => $monitor->update(['enabled' => false]));\n            }, 'disable'),\n\n            Action::make(__('Delete'), function (Enumerable $models): void {\n                $models->each(fn (CveMonitor $monitor) => $monitor->delete());\n            }, 'delete'),\n        ];\n\n        if (ce()) {\n            $actions[] = Action::make(__('Import all CVE\\'s'), function (): void {\n                $importer = app(ImportAllCves::class);\n                $importer->import(0);\n            }, 'import')->standalone();\n        }\n\n        return $actions;\n    }\n\n    protected function applySelect(Builder $builder): static\n    {\n        parent::applySelect($builder);\n\n        $builder->addSelect(\n            DB::raw('(\n    SELECT COUNT(`cve_monitor_matches`.`id`)\n    FROM `cve_monitor_matches`\n    WHERE `cve_monitor_matches`.`cve_monitor_id` = `cve_monitors`.`id`\n) AS total_matches')\n        );\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Models/Cve.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\n\n/**\n * @property int $id\n * @property string $identifier\n * @property ?float $score\n * @property string $description\n * @property Carbon $published_at\n * @property Carbon $modified_at\n * @property array $data\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property Collection<int, CveMonitorMatch> $matches\n */\nclass Cve extends Model\n{\n    protected $guarded = [];\n\n    protected $casts = [\n        'score' => 'float',\n        'published_at' => 'datetime',\n        'modified_at' => 'datetime',\n        'data' => 'array',\n    ];\n\n    public function matches(): HasMany\n    {\n        return $this->hasMany(CveMonitorMatch::class);\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Models/CveMonitor.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Cve\\Observers\\CveMonitorObserver;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Observers\\TeamObserver;\n\n/**\n * @property int $id\n * @property ?int $site_id\n * @property int $team_id\n * @property bool $enabled\n * @property string $keyword\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property Site $site\n * @property Team $team\n */\n#[ScopedBy(TeamScope::class)]\n#[ObservedBy([TeamObserver::class, CveMonitorObserver::class])]\nclass CveMonitor extends Model\n{\n    protected $guarded = [];\n\n    protected $casts = [\n        'enabled' => 'boolean',\n    ];\n\n    public function matches(): HasMany\n    {\n        return $this->hasMany(CveMonitorMatch::class);\n    }\n\n    public function site(): BelongsTo\n    {\n        return $this->belongsTo(Site::class);\n    }\n\n    public function team(): BelongsTo\n    {\n        return $this->belongsTo(Team::class);\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Models/CveMonitorMatch.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Support\\Carbon;\n\n/**\n * @property int $id\n * @property int $cve_id\n * @property int $cve_monitor_id\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property Cve $cve\n * @property CveMonitor $cveMonitor\n */\nclass CveMonitorMatch extends Model\n{\n    protected $guarded = [];\n\n    public function cve(): BelongsTo\n    {\n        return $this->belongsTo(Cve::class);\n    }\n\n    public function cveMonitor(): BelongsTo\n    {\n        return $this->belongsTo(CveMonitor::class);\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Notifications/Conditions/KeywordCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Notifications\\Conditions;\n\nuse Vigilant\\Cve\\Models\\CveMonitor;\nuse Vigilant\\Cve\\Notifications\\CveMatchedNotification;\nuse Vigilant\\Notifications\\Conditions\\SelectCondition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass KeywordCondition extends SelectCondition\n{\n    public static string $name = 'Keyword';\n\n    public function options(): array\n    {\n        return CveMonitor::query()\n            ->pluck('keyword', 'id')\n            ->unique()\n            ->toArray();\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CveMatchedNotification $notification */\n\n        return $notification->monitor->id === $value;\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Notifications/Conditions/ScoreCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Notifications\\Conditions;\n\nuse Vigilant\\Cve\\Notifications\\CveMatchedNotification;\nuse Vigilant\\Notifications\\Conditions\\SelectCondition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass ScoreCondition extends SelectCondition\n{\n    public static string $name = 'Score';\n\n    public function options(): array\n    {\n        return range(0, 10);\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '<>' => 'Not equal to',\n            '<' => 'Less than',\n            '<=' => 'Less or equal than',\n            '>' => 'Greater than',\n            '>=' => 'Greater or equal than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CveMatchedNotification $notification */\n        $score = $notification->cve->score ?? 0;\n\n        return match ($operator) {\n            '=' => $score == $value,\n            '<>' => $score != $value,\n            '<' => $score < $value,\n            '<=' => $score <= $value,\n            '>' => $score > $value,\n            '>=' => $score >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Notifications/CveMatchedNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Notifications;\n\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Models\\CveMonitor;\nuse Vigilant\\Cve\\Notifications\\Conditions\\ScoreCondition;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass CveMatchedNotification extends Notification implements HasSite\n{\n    public static string $name = 'CVE matched monitored keyword';\n\n    public static ?int $defaultCooldown = 60;\n\n    public static array $defaultConditions = [\n        'type' => 'group',\n        'children' => [\n            [\n                'type' => 'condition',\n                'condition' => ScoreCondition::class,\n                'operator' => '>=',\n                'value' => 6,\n            ],\n        ],\n    ];\n\n    public function __construct(public CveMonitor $monitor, public Cve $cve) {}\n\n    public function title(): string\n    {\n        return __(':id with a score of :score found', [\n            'id' => $this->cve->identifier,\n            'score' => $this->cve->score ?? 0,\n        ]);\n    }\n\n    public function description(): string\n    {\n        $description = __('The CVE :id was published at :publishedAt and has a score of :score and matched your monitored keyword :keyword.', [\n            'id' => $this->cve->identifier,\n            'publishedAt' => $this->cve->published_at->toDateString(),\n            'keyword' => $this->monitor->keyword,\n            'score' => $this->cve->score ?? 0,\n        ]);\n\n        $description .= \"\\n\\n\";\n        $description .= __('Description: :description', [\n            'description' => str($this->cve->description)->limit(500),\n        ]);\n\n        return $description;\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when a new CVE is published matching your monitored keywords.');\n    }\n\n    public function level(): Level\n    {\n        if ($this->cve->score >= 7) {\n            return Level::Critical;\n        }\n\n        if ($this->cve->score >= 4) {\n            return Level::Warning;\n        }\n\n        return Level::Info;\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->cve->id;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->monitor->site;\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/Observers/CveMonitorObserver.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Observers;\n\nuse Vigilant\\Cve\\Jobs\\MatchExistingCvesJob;\nuse Vigilant\\Cve\\Models\\CveMonitor;\n\nclass CveMonitorObserver\n{\n    public function created(CveMonitor $monitor): void\n    {\n        MatchExistingCvesJob::dispatch($monitor);\n    }\n\n    public function updated(CveMonitor $monitor): void\n    {\n        $monitor->matches()->delete();\n        MatchExistingCvesJob::dispatch($monitor);\n    }\n}\n"
  },
  {
    "path": "packages/cve/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve;\n\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\nuse Livewire\\Livewire;\nuse Vigilant\\Core\\Facades\\Navigation;\nuse Vigilant\\Core\\Policies\\AllowAllPolicy;\nuse Vigilant\\Cve\\Commands\\ImportAllCvesCommand;\nuse Vigilant\\Cve\\Commands\\ImportCvesCommand;\nuse Vigilant\\Cve\\Commands\\MatchCveCommand;\nuse Vigilant\\Cve\\Commands\\MatchExistingCvesCommand;\nuse Vigilant\\Cve\\Livewire\\CveMonitorForm;\nuse Vigilant\\Cve\\Livewire\\Tables\\CveMonitorMatchesTable;\nuse Vigilant\\Cve\\Livewire\\Tables\\CveMonitorTable;\nuse Vigilant\\Cve\\Models\\CveMonitor;\nuse Vigilant\\Cve\\Notifications\\Conditions\\KeywordCondition;\nuse Vigilant\\Cve\\Notifications\\Conditions\\ScoreCondition;\nuse Vigilant\\Cve\\Notifications\\CveMatchedNotification;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Users\\Models\\User;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function register(): void\n    {\n        $this\n            ->registerConfig();\n    }\n\n    protected function registerConfig(): static\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/cve.php', 'cve');\n\n        return $this;\n    }\n\n    public function boot(): void\n    {\n        $this\n            ->bootRoutes()\n            ->bootConfig()\n            ->bootMigrations()\n            ->bootCommands()\n            ->bootViews()\n            ->bootLivewire()\n            ->bootNavigation()\n            ->bootNotifications()\n            ->bootGates()\n            ->bootPolicies();\n    }\n\n    protected function bootRoutes(): static\n    {\n        if (! $this->app->routesAreCached()) {\n            Route::middleware(['web', 'auth'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/web.php'));\n        }\n\n        return $this;\n    }\n\n    protected function bootConfig(): static\n    {\n        $this->publishes([\n            __DIR__.'/../config/cve.php' => config_path('cve.php'),\n        ], 'config');\n\n        return $this;\n    }\n\n    protected function bootMigrations(): static\n    {\n        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');\n\n        return $this;\n    }\n\n    protected function bootCommands(): static\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n                ImportAllCvesCommand::class,\n                ImportCvesCommand::class,\n                MatchExistingCvesCommand::class,\n                MatchCveCommand::class,\n            ]);\n        }\n\n        return $this;\n    }\n\n    protected function bootViews(): static\n    {\n        $this->loadViewsFrom(__DIR__.'/../resources/views', 'cve');\n\n        return $this;\n    }\n\n    protected function bootLivewire(): static\n    {\n        Livewire::component('cve-monitor-table', CveMonitorTable::class);\n        Livewire::component('cve-monitor-form', CveMonitorForm::class);\n\n        Livewire::component('cve-monitor-matches-table', CveMonitorMatchesTable::class);\n\n        return $this;\n    }\n\n    protected function bootNavigation(): static\n    {\n        Navigation::path(__DIR__.'/../resources/navigation.php');\n\n        return $this;\n    }\n\n    protected function bootNotifications(): static\n    {\n        NotificationRegistry::registerNotification([\n            CveMatchedNotification::class,\n        ]);\n\n        NotificationRegistry::registerCondition(CveMatchedNotification::class, [\n            ScoreCondition::class,\n            KeywordCondition::class,\n        ]);\n\n        return $this;\n    }\n\n    protected function bootGates(): static\n    {\n        if (ce()) {\n            Gate::define('use-cve', function (User $user): bool {\n                return ce();\n            });\n        }\n\n        return $this;\n    }\n\n    protected function bootPolicies(): static\n    {\n        if (ce()) {\n            Gate::policy(CveMonitor::class, AllowAllPolicy::class);\n        }\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/cve/testbench.yaml",
    "content": "providers:\n  - Vigilant\\Cve\\ServiceProvider\n"
  },
  {
    "path": "packages/cve/tests/Actions/ImportAllCvesTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Tests\\Actions;\n\nuse Illuminate\\Support\\Facades\\Bus;\nuse Illuminate\\Support\\Facades\\Http;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Cve\\Actions\\ImportAllCves;\nuse Vigilant\\Cve\\Jobs\\ImportAllCvesJob;\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Tests\\TestCase;\n\nclass ImportAllCvesTest extends TestCase\n{\n    #[Test]\n    public function it_imports_full_page_of_cves(): void\n    {\n        Bus::fake();\n\n        $vulnerabilities = [];\n        for ($i = 0; $i < 500; $i++) {\n            $vulnerabilities[] = [\n                'cve' => [\n                    'id' => \"CVE-2024-{$i}\",\n                    'descriptions' => [\n                        ['lang' => 'en', 'value' => \"Vulnerability {$i}\"],\n                    ],\n                    'metrics' => [\n                        'cvssMetricV31' => [\n                            ['cvssData' => ['baseScore' => 5.0]],\n                        ],\n                    ],\n                    'published' => '2024-01-01T00:00:00Z',\n                    'lastModified' => '2024-01-02T00:00:00Z',\n                ],\n            ];\n        }\n\n        Http::fake([\n            'services.nvd.nist.gov/*' => Http::response([\n                'vulnerabilities' => $vulnerabilities,\n            ]),\n        ])->preventStrayRequests();\n\n        /** @var ImportAllCves $action */\n        $action = app(ImportAllCves::class);\n        $action->import(0);\n\n        // Should import all 500 CVEs\n        $this->assertEquals(500, Cve::query()->count());\n\n        // Should dispatch next page job\n        Bus::assertDispatched(ImportAllCvesJob::class);\n    }\n\n    #[Test]\n    public function it_does_not_dispatch_next_job_for_partial_page(): void\n    {\n        Bus::fake();\n\n        $vulnerabilities = [];\n        for ($i = 0; $i < 250; $i++) {\n            $vulnerabilities[] = [\n                'cve' => [\n                    'id' => \"CVE-2024-{$i}\",\n                    'descriptions' => [\n                        ['lang' => 'en', 'value' => \"Vulnerability {$i}\"],\n                    ],\n                    'metrics' => [],\n                    'published' => '2024-01-01T00:00:00Z',\n                    'lastModified' => '2024-01-02T00:00:00Z',\n                ],\n            ];\n        }\n\n        Http::fake([\n            'services.nvd.nist.gov/*' => Http::response([\n                'vulnerabilities' => $vulnerabilities,\n            ]),\n        ])->preventStrayRequests();\n\n        /** @var ImportAllCves $action */\n        $action = app(ImportAllCves::class);\n        $action->import(0);\n\n        // Should import all 250 CVEs\n        $this->assertEquals(250, Cve::query()->count());\n\n        // Should NOT dispatch next page job (less than 500)\n        Bus::assertNotDispatched(ImportAllCvesJob::class);\n    }\n\n    #[Test]\n    public function it_uses_correct_pagination_parameters(): void\n    {\n        Http::fake([\n            'services.nvd.nist.gov/*' => Http::response([\n                'vulnerabilities' => [],\n            ]),\n        ])->preventStrayRequests();\n\n        /** @var ImportAllCves $action */\n        $action = app(ImportAllCves::class);\n        $action->import(3);\n\n        Http::assertSent(function ($request) {\n            $url = $request->url();\n\n            return str_contains($url, 'resultsPerPage=500') &&\n                   str_contains($url, 'startIndex=1500'); // 3 * 500\n        });\n    }\n\n    #[Test]\n    public function it_does_not_notify_for_bulk_import(): void\n    {\n        Bus::fake();\n\n        Http::fake([\n            'services.nvd.nist.gov/*' => Http::response([\n                'vulnerabilities' => [\n                    [\n                        'cve' => [\n                            'id' => 'CVE-2024-NEW',\n                            'descriptions' => [\n                                ['lang' => 'en', 'value' => 'Recent vulnerability'],\n                            ],\n                            'metrics' => [\n                                'cvssMetricV31' => [\n                                    ['cvssData' => ['baseScore' => 9.0]],\n                                ],\n                            ],\n                            'published' => now()->subDays(2)->toIso8601String(),\n                            'lastModified' => now()->toIso8601String(),\n                        ],\n                    ],\n                ],\n            ]),\n        ])->preventStrayRequests();\n\n        /** @var ImportAllCves $action */\n        $action = app(ImportAllCves::class);\n        $action->import(0);\n\n        // Should not dispatch match job even for recent CVE\n        Bus::assertNotDispatched(\\Vigilant\\Cve\\Jobs\\MatchCveMonitorsJob::class);\n    }\n\n    #[Test]\n    public function it_handles_empty_response(): void\n    {\n        Bus::fake();\n\n        Http::fake([\n            'services.nvd.nist.gov/*' => Http::response([\n                'vulnerabilities' => [],\n            ]),\n        ])->preventStrayRequests();\n\n        /** @var ImportAllCves $action */\n        $action = app(ImportAllCves::class);\n        $action->import(0);\n\n        $this->assertEquals(0, Cve::query()->count());\n        Bus::assertNotDispatched(ImportAllCvesJob::class);\n    }\n}\n"
  },
  {
    "path": "packages/cve/tests/Actions/ImportCveTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Tests\\Actions;\n\nuse Illuminate\\Support\\Facades\\Bus;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Cve\\Actions\\ImportCve;\nuse Vigilant\\Cve\\Jobs\\MatchCveMonitorsJob;\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Tests\\TestCase;\n\nclass ImportCveTest extends TestCase\n{\n    #[Test]\n    public function it_imports_cve_with_english_description(): void\n    {\n        $cveData = [\n            'cve' => [\n                'id' => 'CVE-2024-1234',\n                'descriptions' => [\n                    ['lang' => 'en', 'value' => 'Test vulnerability description'],\n                    ['lang' => 'es', 'value' => 'Descripción de vulnerabilidad'],\n                ],\n                'metrics' => [\n                    'cvssMetricV31' => [\n                        ['cvssData' => ['baseScore' => 7.5]],\n                    ],\n                ],\n                'published' => '2024-01-01T00:00:00Z',\n                'lastModified' => '2024-01-02T00:00:00Z',\n            ],\n        ];\n\n        /** @var ImportCve $action */\n        $action = app(ImportCve::class);\n        $action->import($cveData);\n\n        $this->assertDatabaseHas('cves', [\n            'identifier' => 'CVE-2024-1234',\n            'description' => 'Test vulnerability description',\n            'score' => 7.5,\n        ]);\n    }\n\n    #[Test]\n    public function it_imports_cve_without_english_description(): void\n    {\n        $cveData = [\n            'cve' => [\n                'id' => 'CVE-2024-5678',\n                'descriptions' => [\n                    ['lang' => 'es', 'value' => 'Primera descripción'],\n                ],\n                'metrics' => [\n                    'cvssMetricV2' => [\n                        ['cvssData' => ['baseScore' => 5.0]],\n                    ],\n                ],\n                'published' => '2024-01-01T00:00:00Z',\n                'lastModified' => '2024-01-02T00:00:00Z',\n            ],\n        ];\n\n        /** @var ImportCve $action */\n        $action = app(ImportCve::class);\n        $action->import($cveData);\n\n        $this->assertDatabaseHas('cves', [\n            'identifier' => 'CVE-2024-5678',\n            'description' => 'Primera descripción',\n            'score' => 5.0,\n        ]);\n    }\n\n    #[Test]\n    public function it_uses_cvss_v2_when_v3_not_available(): void\n    {\n        $cveData = [\n            'cve' => [\n                'id' => 'CVE-2024-9999',\n                'descriptions' => [\n                    ['lang' => 'en', 'value' => 'Test description'],\n                ],\n                'metrics' => [\n                    'cvssMetricV2' => [\n                        ['cvssData' => ['baseScore' => 4.3]],\n                    ],\n                ],\n                'published' => '2024-01-01T00:00:00Z',\n                'lastModified' => '2024-01-02T00:00:00Z',\n            ],\n        ];\n\n        /** @var ImportCve $action */\n        $action = app(ImportCve::class);\n        $action->import($cveData);\n\n        $cve = Cve::query()->where('identifier', 'CVE-2024-9999')->first();\n        $this->assertNotNull($cve);\n        $this->assertEquals(4.3, $cve->score);\n    }\n\n    #[Test]\n    public function it_updates_existing_cve(): void\n    {\n        Cve::query()->create([\n            'identifier' => 'CVE-2024-1111',\n            'description' => 'Old description',\n            'score' => 1.0,\n            'published_at' => '2024-01-01',\n            'modified_at' => '2024-01-01',\n            'data' => [],\n        ]);\n\n        $cveData = [\n            'cve' => [\n                'id' => 'CVE-2024-1111',\n                'descriptions' => [\n                    ['lang' => 'en', 'value' => 'Updated description'],\n                ],\n                'metrics' => [\n                    'cvssMetricV31' => [\n                        ['cvssData' => ['baseScore' => 9.0]],\n                    ],\n                ],\n                'published' => '2024-01-01T00:00:00Z',\n                'lastModified' => '2024-01-05T00:00:00Z',\n            ],\n        ];\n\n        /** @var ImportCve $action */\n        $action = app(ImportCve::class);\n        $action->import($cveData);\n\n        $cve = Cve::query()->where('identifier', 'CVE-2024-1111')->first();\n        $this->assertNotNull($cve);\n        $this->assertEquals('Updated description', $cve->description);\n        $this->assertEquals(9.0, $cve->score);\n    }\n\n    #[Test]\n    public function it_dispatches_match_job_for_recent_cves(): void\n    {\n        Bus::fake();\n\n        $cveData = [\n            'cve' => [\n                'id' => 'CVE-2024-NEW',\n                'descriptions' => [\n                    ['lang' => 'en', 'value' => 'Recent vulnerability'],\n                ],\n                'metrics' => [\n                    'cvssMetricV31' => [\n                        ['cvssData' => ['baseScore' => 8.0]],\n                    ],\n                ],\n                'published' => now()->subDays(2)->toIso8601String(),\n                'lastModified' => now()->toIso8601String(),\n            ],\n        ];\n\n        /** @var ImportCve $action */\n        $action = app(ImportCve::class);\n        $action->import($cveData, notify: true);\n\n        Bus::assertDispatched(MatchCveMonitorsJob::class);\n    }\n\n    #[Test]\n    public function it_does_not_dispatch_match_job_for_old_cves(): void\n    {\n        Bus::fake();\n\n        $cveData = [\n            'cve' => [\n                'id' => 'CVE-2020-OLD',\n                'descriptions' => [\n                    ['lang' => 'en', 'value' => 'Old vulnerability'],\n                ],\n                'metrics' => [\n                    'cvssMetricV31' => [\n                        ['cvssData' => ['baseScore' => 8.0]],\n                    ],\n                ],\n                'published' => now()->subMonths(6)->toIso8601String(),\n                'lastModified' => now()->toIso8601String(),\n            ],\n        ];\n\n        /** @var ImportCve $action */\n        $action = app(ImportCve::class);\n        $action->import($cveData, notify: true);\n\n        Bus::assertNotDispatched(MatchCveMonitorsJob::class);\n    }\n\n    #[Test]\n    public function it_does_not_dispatch_match_job_when_notify_is_false(): void\n    {\n        Bus::fake();\n\n        $cveData = [\n            'cve' => [\n                'id' => 'CVE-2024-NONOTIFY',\n                'descriptions' => [\n                    ['lang' => 'en', 'value' => 'Recent vulnerability'],\n                ],\n                'metrics' => [\n                    'cvssMetricV31' => [\n                        ['cvssData' => ['baseScore' => 8.0]],\n                    ],\n                ],\n                'published' => now()->subDays(2)->toIso8601String(),\n                'lastModified' => now()->toIso8601String(),\n            ],\n        ];\n\n        /** @var ImportCve $action */\n        $action = app(ImportCve::class);\n        $action->import($cveData, notify: false);\n\n        Bus::assertNotDispatched(MatchCveMonitorsJob::class);\n    }\n\n    #[Test]\n    public function it_handles_missing_score(): void\n    {\n        $cveData = [\n            'cve' => [\n                'id' => 'CVE-2024-NOSCORE',\n                'descriptions' => [\n                    ['lang' => 'en', 'value' => 'Vulnerability without score'],\n                ],\n                'metrics' => [],\n                'published' => '2024-01-01T00:00:00Z',\n                'lastModified' => '2024-01-02T00:00:00Z',\n            ],\n        ];\n\n        /** @var ImportCve $action */\n        $action = app(ImportCve::class);\n        $action->import($cveData);\n\n        $cve = Cve::query()->where('identifier', 'CVE-2024-NOSCORE')->first();\n        $this->assertNotNull($cve);\n        $this->assertNull($cve->score);\n    }\n}\n"
  },
  {
    "path": "packages/cve/tests/Actions/ImportCvesTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Tests\\Actions;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Cve\\Actions\\ImportCves;\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Tests\\TestCase;\n\nclass ImportCvesTest extends TestCase\n{\n    #[Test]\n    public function it_imports_cves_from_api(): void\n    {\n        Http::fake([\n            'services.nvd.nist.gov/*' => Http::response([\n                'vulnerabilities' => [\n                    [\n                        'cve' => [\n                            'id' => 'CVE-2024-0001',\n                            'descriptions' => [\n                                ['lang' => 'en', 'value' => 'First vulnerability'],\n                            ],\n                            'metrics' => [\n                                'cvssMetricV31' => [\n                                    ['cvssData' => ['baseScore' => 7.5]],\n                                ],\n                            ],\n                            'published' => '2024-01-01T00:00:00Z',\n                            'lastModified' => '2024-01-02T00:00:00Z',\n                        ],\n                    ],\n                    [\n                        'cve' => [\n                            'id' => 'CVE-2024-0002',\n                            'descriptions' => [\n                                ['lang' => 'en', 'value' => 'Second vulnerability'],\n                            ],\n                            'metrics' => [\n                                'cvssMetricV31' => [\n                                    ['cvssData' => ['baseScore' => 5.0]],\n                                ],\n                            ],\n                            'published' => '2024-01-02T00:00:00Z',\n                            'lastModified' => '2024-01-03T00:00:00Z',\n                        ],\n                    ],\n                ],\n            ]),\n        ])->preventStrayRequests();\n\n        /** @var ImportCves $action */\n        $action = app(ImportCves::class);\n        $action->import(now()->subDay());\n\n        $this->assertDatabaseHas('cves', [\n            'identifier' => 'CVE-2024-0001',\n        ]);\n\n        $this->assertDatabaseHas('cves', [\n            'identifier' => 'CVE-2024-0002',\n        ]);\n    }\n\n    #[Test]\n    public function it_uses_latest_cve_date_when_from_is_null(): void\n    {\n        Cve::query()->create([\n            'identifier' => 'CVE-2023-9999',\n            'description' => 'Latest existing CVE',\n            'score' => 5.0,\n            'published_at' => '2023-12-15 00:00:00',\n            'modified_at' => '2023-12-15 00:00:00',\n            'data' => [],\n        ]);\n\n        Http::fake([\n            'services.nvd.nist.gov/*' => Http::response([\n                'vulnerabilities' => [],\n            ]),\n        ])->preventStrayRequests();\n\n        /** @var ImportCves $action */\n        $action = app(ImportCves::class);\n        $action->import(null);\n\n        Http::assertSent(function ($request) {\n            return str_contains($request->url(), 'pubStartDate=2023-12-15');\n        });\n    }\n\n    #[Test]\n    public function it_uses_yesterday_when_no_cves_exist(): void\n    {\n        Http::fake([\n            'services.nvd.nist.gov/*' => Http::response([\n                'vulnerabilities' => [],\n            ]),\n        ])->preventStrayRequests();\n\n        /** @var ImportCves $action */\n        $action = app(ImportCves::class);\n        $action->import(null);\n\n        Http::assertSent(function ($request) {\n            $yesterday = now()->subDay()->format('Y-m-d');\n\n            return str_contains($request->url(), \"pubStartDate={$yesterday}\");\n        });\n    }\n\n    #[Test]\n    public function it_limits_date_range_to_30_days(): void\n    {\n        Http::fake([\n            'services.nvd.nist.gov/*' => Http::response([\n                'vulnerabilities' => [],\n            ]),\n        ])->preventStrayRequests();\n\n        $from = now()->subDays(60);\n\n        /** @var ImportCves $action */\n        $action = app(ImportCves::class);\n        $action->import($from);\n\n        Http::assertSent(function ($request) use ($from) {\n            $expectedEnd = $from->clone()->addDays(30)->format('Y-m-d');\n\n            return str_contains($request->url(), \"pubEndDate={$expectedEnd}\");\n        });\n    }\n\n    #[Test]\n    public function it_limits_end_date_to_now_when_in_future(): void\n    {\n        Http::fake([\n            'services.nvd.nist.gov/*' => Http::response([\n                'vulnerabilities' => [],\n            ]),\n        ])->preventStrayRequests();\n\n        $from = now()->subDays(10);\n\n        /** @var ImportCves $action */\n        $action = app(ImportCves::class);\n        $action->import($from);\n\n        Http::assertSent(function ($request) {\n            $today = now()->format('Y-m-d');\n\n            return str_contains($request->url(), \"pubEndDate={$today}\");\n        });\n    }\n}\n"
  },
  {
    "path": "packages/cve/tests/Actions/MatchCveTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Tests\\Actions;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Cve\\Actions\\MatchCve;\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Models\\CveMonitor;\nuse Vigilant\\Cve\\Tests\\TestCase;\n\nclass MatchCveTest extends TestCase\n{\n    #[Test]\n    public function it_matches_cve_with_keyword(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'apache',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-1234',\n            'description' => 'Apache HTTP Server vulnerability allows remote code execution',\n            'score' => 9.8,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        /** @var MatchCve $action */\n        $action = app(MatchCve::class);\n        $action->match($monitor, $cve);\n\n        $this->assertDatabaseHas('cve_monitor_matches', [\n            'cve_monitor_id' => $monitor->id,\n            'cve_id' => $cve->id,\n        ]);\n    }\n\n    #[Test]\n    public function it_does_not_match_cve_without_keyword(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'apache',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-5678',\n            'description' => 'nginx vulnerability allows privilege escalation',\n            'score' => 7.2,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        /** @var MatchCve $action */\n        $action = app(MatchCve::class);\n        $action->match($monitor, $cve);\n\n        $this->assertDatabaseMissing('cve_monitor_matches', [\n            'cve_monitor_id' => $monitor->id,\n            'cve_id' => $cve->id,\n        ]);\n    }\n\n    #[Test]\n    public function it_matches_case_insensitively(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'APACHE',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-9999',\n            'description' => 'apache server issue',\n            'score' => 5.0,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        /** @var MatchCve $action */\n        $action = app(MatchCve::class);\n        $action->match($monitor, $cve);\n\n        $this->assertDatabaseHas('cve_monitor_matches', [\n            'cve_monitor_id' => $monitor->id,\n            'cve_id' => $cve->id,\n        ]);\n    }\n\n    #[Test]\n    public function it_does_not_create_duplicate_matches(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'test',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-TEST',\n            'description' => 'test vulnerability',\n            'score' => 5.0,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $monitor->matches()->create([\n            'cve_id' => $cve->id,\n        ]);\n\n        /** @var MatchCve $action */\n        $action = app(MatchCve::class);\n        $action->match($monitor, $cve);\n\n        $this->assertEquals(1, $monitor->matches()->count());\n    }\n\n    #[Test]\n    public function it_matches_partial_keywords(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'sql',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-SQL',\n            'description' => 'MySQL server allows SQL injection attacks',\n            'score' => 8.0,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        /** @var MatchCve $action */\n        $action = app(MatchCve::class);\n        $action->match($monitor, $cve);\n\n        $this->assertDatabaseHas('cve_monitor_matches', [\n            'cve_monitor_id' => $monitor->id,\n            'cve_id' => $cve->id,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/cve/tests/Models/CveMonitorMatchTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Tests\\Models;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Models\\CveMonitor;\nuse Vigilant\\Cve\\Models\\CveMonitorMatch;\nuse Vigilant\\Cve\\Tests\\TestCase;\n\nclass CveMonitorMatchTest extends TestCase\n{\n    #[Test]\n    public function it_can_create_match(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'apache',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-0001',\n            'description' => 'Apache vulnerability',\n            'score' => 7.5,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $match = CveMonitorMatch::query()->create([\n            'cve_monitor_id' => $monitor->id,\n            'cve_id' => $cve->id,\n        ]);\n\n        $this->assertInstanceOf(CveMonitorMatch::class, $match);\n        $this->assertEquals($monitor->id, $match->cve_monitor_id);\n        $this->assertEquals($cve->id, $match->cve_id);\n    }\n\n    #[Test]\n    public function it_belongs_to_cve(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'test',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-0001',\n            'description' => 'Test vulnerability',\n            'score' => 5.0,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $match = CveMonitorMatch::query()->create([\n            'cve_monitor_id' => $monitor->id,\n            'cve_id' => $cve->id,\n        ]);\n\n        $this->assertInstanceOf(Cve::class, $match->cve);\n        $this->assertEquals('CVE-2024-0001', $match->cve->identifier);\n    }\n\n    #[Test]\n    public function it_belongs_to_cve_monitor(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'apache',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-0001',\n            'description' => 'Apache vulnerability',\n            'score' => 7.5,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $match = CveMonitorMatch::query()->create([\n            'cve_monitor_id' => $monitor->id,\n            'cve_id' => $cve->id,\n        ]);\n\n        $this->assertInstanceOf(CveMonitor::class, $match->cveMonitor);\n        $this->assertEquals('apache', $match->cveMonitor->keyword);\n    }\n\n    #[Test]\n    public function it_has_timestamps(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'test',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-0001',\n            'description' => 'Test',\n            'score' => 5.0,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $match = CveMonitorMatch::query()->create([\n            'cve_monitor_id' => $monitor->id,\n            'cve_id' => $cve->id,\n        ]);\n\n        $this->assertNotNull($match->created_at);\n        $this->assertNotNull($match->updated_at);\n    }\n}\n"
  },
  {
    "path": "packages/cve/tests/Models/CveMonitorTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Tests\\Models;\n\nuse Illuminate\\Support\\Facades\\Bus;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Models\\CveMonitor;\nuse Vigilant\\Cve\\Models\\CveMonitorMatch;\nuse Vigilant\\Cve\\Tests\\TestCase;\n\nclass CveMonitorTest extends TestCase\n{\n    #[Test]\n    public function it_can_create_monitor(): void\n    {\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'apache',\n            'enabled' => true,\n        ]);\n\n        $this->assertInstanceOf(CveMonitor::class, $monitor);\n        $this->assertEquals('apache', $monitor->keyword);\n        $this->assertTrue($monitor->enabled);\n    }\n\n    #[Test]\n    public function it_casts_enabled_to_boolean(): void\n    {\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'test',\n            'enabled' => 1,\n        ]);\n\n        $this->assertTrue($monitor->enabled);\n    }\n\n    #[Test]\n    public function it_has_matches_relationship(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'apache',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-0001',\n            'description' => 'Apache vulnerability',\n            'score' => 7.5,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $monitor->matches()->create([\n            'cve_id' => $cve->id,\n        ]);\n\n        $this->assertEquals(1, $monitor->matches()->count());\n        $this->assertInstanceOf(CveMonitorMatch::class, $monitor->matches->first());\n    }\n\n    #[Test]\n    public function it_can_have_multiple_matches(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'apache',\n            'enabled' => true,\n        ]);\n\n        for ($i = 1; $i <= 5; $i++) {\n            $cve = Cve::query()->create([\n                'identifier' => \"CVE-2024-000{$i}\",\n                'description' => 'Apache vulnerability',\n                'score' => 7.5,\n                'published_at' => now(),\n                'modified_at' => now(),\n                'data' => [],\n            ]);\n\n            $monitor->matches()->create([\n                'cve_id' => $cve->id,\n            ]);\n        }\n\n        $this->assertEquals(5, $monitor->matches()->count());\n    }\n\n    #[Test]\n    public function it_can_be_disabled(): void\n    {\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'test',\n            'enabled' => false,\n        ]);\n\n        $this->assertFalse($monitor->enabled);\n    }\n\n    #[Test]\n    public function it_defaults_enabled_to_false(): void\n    {\n        Bus::fake(); // Prevent observer from firing\n\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'test',\n        ]);\n\n        // Check the actual database value since the model might have default behavior\n        $this->assertNotNull($monitor->fresh());\n    }\n}\n"
  },
  {
    "path": "packages/cve/tests/Models/CveTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Tests\\Models;\n\nuse Illuminate\\Support\\Facades\\Bus;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Models\\CveMonitor;\nuse Vigilant\\Cve\\Models\\CveMonitorMatch;\nuse Vigilant\\Cve\\Tests\\TestCase;\n\nclass CveTest extends TestCase\n{\n    #[Test]\n    public function it_can_create_cve(): void\n    {\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-1234',\n            'description' => 'Test vulnerability',\n            'score' => 7.5,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => ['test' => 'data'],\n        ]);\n\n        $this->assertInstanceOf(Cve::class, $cve);\n        $this->assertEquals('CVE-2024-1234', $cve->identifier);\n        $this->assertEquals(7.5, $cve->score);\n        $this->assertNotEmpty($cve->data);\n    }\n\n    #[Test]\n    public function it_casts_score_to_float(): void\n    {\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-5678',\n            'description' => 'Test',\n            'score' => '9.8',\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $this->assertEquals(9.8, $cve->score);\n    }\n\n    #[Test]\n    public function it_casts_dates_correctly(): void\n    {\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-9999',\n            'description' => 'Test',\n            'score' => 5.0,\n            'published_at' => '2024-01-01 00:00:00',\n            'modified_at' => '2024-01-02 00:00:00',\n            'data' => [],\n        ]);\n\n        $this->assertInstanceOf(\\Illuminate\\Support\\Carbon::class, $cve->published_at);\n        $this->assertInstanceOf(\\Illuminate\\Support\\Carbon::class, $cve->modified_at);\n    }\n\n    #[Test]\n    public function it_has_matches_relationship(): void\n    {\n        Bus::fake(); // Fake bus to prevent observer from firing\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-0001',\n            'description' => 'Test vulnerability',\n            'score' => 7.5,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'test',\n            'enabled' => true,\n        ]);\n\n        $cve->matches()->create([\n            'cve_monitor_id' => $monitor->id,\n        ]);\n\n        $this->assertEquals(1, $cve->matches()->count());\n        $this->assertInstanceOf(CveMonitorMatch::class, $cve->matches->first());\n    }\n\n    #[Test]\n    public function it_handles_null_score(): void\n    {\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-NULL',\n            'description' => 'Test',\n            'score' => null,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $this->assertNull($cve->score);\n    }\n\n    #[Test]\n    public function it_stores_json_data(): void\n    {\n        $data = [\n            'cve' => [\n                'id' => 'CVE-2024-JSON',\n                'metrics' => ['cvssV3' => 7.5],\n            ],\n        ];\n\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-JSON',\n            'description' => 'Test',\n            'score' => 7.5,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => $data,\n        ]);\n\n        $this->assertEquals($data, $cve->data);\n        $this->assertEquals('CVE-2024-JSON', $cve->data['cve']['id']);\n    }\n}\n"
  },
  {
    "path": "packages/cve/tests/Notifications/CveMatchedNotificationTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Tests\\Notifications;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Cve\\Models\\Cve;\nuse Vigilant\\Cve\\Models\\CveMonitor;\nuse Vigilant\\Cve\\Notifications\\CveMatchedNotification;\nuse Vigilant\\Cve\\Tests\\TestCase;\nuse Vigilant\\Notifications\\Enums\\Level;\n\nclass CveMatchedNotificationTest extends TestCase\n{\n    #[Test]\n    public function it_creates_notification(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'apache',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-1234',\n            'description' => 'Apache HTTP Server vulnerability',\n            'score' => 7.5,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $notification = new CveMatchedNotification($monitor, $cve);\n\n        $this->assertInstanceOf(CveMatchedNotification::class, $notification);\n        $this->assertEquals($monitor->id, $notification->monitor->id);\n        $this->assertEquals($cve->id, $notification->cve->id);\n    }\n\n    #[Test]\n    public function it_returns_correct_title(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'apache',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-1234',\n            'description' => 'Test vulnerability',\n            'score' => 8.5,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $notification = new CveMatchedNotification($monitor, $cve);\n        $title = $notification->title();\n\n        $this->assertStringContainsString('CVE-2024-1234', $title);\n        $this->assertStringContainsString('8.5', $title);\n    }\n\n    #[Test]\n    public function it_returns_correct_description(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'apache',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-5678',\n            'description' => 'Apache HTTP Server remote code execution vulnerability',\n            'score' => 9.8,\n            'published_at' => now()->subDays(2),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $notification = new CveMatchedNotification($monitor, $cve);\n        $description = $notification->description();\n\n        $this->assertStringContainsString('CVE-2024-5678', $description);\n        $this->assertStringContainsString('apache', $description);\n        $this->assertStringContainsString('9.8', $description);\n        $this->assertStringContainsString('Apache HTTP Server', $description);\n    }\n\n    #[Test]\n    public function it_returns_critical_level_for_high_score(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'test',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-CRITICAL',\n            'description' => 'Critical vulnerability',\n            'score' => 9.5,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $notification = new CveMatchedNotification($monitor, $cve);\n\n        $this->assertEquals(Level::Critical, $notification->level());\n    }\n\n    #[Test]\n    public function it_returns_warning_level_for_medium_score(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'test',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-WARNING',\n            'description' => 'Medium vulnerability',\n            'score' => 5.5,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $notification = new CveMatchedNotification($monitor, $cve);\n\n        $this->assertEquals(Level::Warning, $notification->level());\n    }\n\n    #[Test]\n    public function it_returns_info_level_for_low_score(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'test',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-INFO',\n            'description' => 'Low vulnerability',\n            'score' => 2.5,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $notification = new CveMatchedNotification($monitor, $cve);\n\n        $this->assertEquals(Level::Info, $notification->level());\n    }\n\n    #[Test]\n    public function it_returns_unique_id(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'test',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-UNIQUE',\n            'description' => 'Test',\n            'score' => 5.0,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $notification = new CveMatchedNotification($monitor, $cve);\n\n        $this->assertEquals($cve->id, $notification->uniqueId());\n    }\n\n    #[Test]\n    public function it_handles_null_score(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'test',\n            'enabled' => true,\n        ]);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-NULL',\n            'description' => 'Test',\n            'score' => null,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $notification = new CveMatchedNotification($monitor, $cve);\n\n        $title = $notification->title();\n        $this->assertStringContainsString('0', $title);\n\n        $this->assertEquals(Level::Info, $notification->level());\n    }\n\n    #[Test]\n    public function it_truncates_long_descriptions(): void\n    {\n        /** @var CveMonitor $monitor */\n        $monitor = CveMonitor::query()->create([\n            'keyword' => 'test',\n            'enabled' => true,\n        ]);\n\n        $longDescription = str_repeat('This is a very long vulnerability description. ', 50);\n\n        /** @var Cve $cve */\n        $cve = Cve::query()->create([\n            'identifier' => 'CVE-2024-LONG',\n            'description' => $longDescription,\n            'score' => 5.0,\n            'published_at' => now(),\n            'modified_at' => now(),\n            'data' => [],\n        ]);\n\n        $notification = new CveMatchedNotification($monitor, $cve);\n        $description = $notification->description();\n\n        // Should be truncated (check for reasonable length, accounting for extra text)\n        $this->assertLessThanOrEqual(700, strlen($description));\n        $this->assertStringContainsString('...', $description);\n    }\n}\n"
  },
  {
    "path": "packages/cve/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\Cve\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Livewire\\LivewireServiceProvider;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Cve\\ServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        TeamService::fake();\n    }\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n            \\Vigilant\\Users\\ServiceProvider::class,\n            \\Vigilant\\Notifications\\ServiceProvider::class,\n            LivewireServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/dns/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/dns/composer.json",
    "content": "{\n    \"name\": \"vigilant/dns\",\n    \"description\": \"Vigilant DNS\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"guzzlehttp/guzzle\": \"^7.8\",\n        \"laravel/framework\": \"^12.0\",\n        \"livewire/livewire\": \"^3.4\",\n        \"vigilant/core\": \"@dev\",\n        \"vigilant/sites\": \"@dev\",\n        \"vigilant/users\": \"@dev\",\n        \"vigilant/frontend\": \"@dev\",\n        \"vigilant/notifications\": \"@dev\",\n        \"bluelibraries/dns\": \"@dev\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Dns\\\\\": \"src\",\n            \"Vigilant\\\\Dns\\\\Database\\\\Factories\\\\\": \"database/factories\",\n            \"Vigilant\\\\Users\\\\Database\\\\Factories\\\\\": \"../users/database/factories\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Dns\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\Dns\\\\ServiceProvider\"\n            ]\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        },\n        {\n            \"type\": \"vcs\",\n            \"url\": \"git@github.com:VincentBean/dns.git\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/dns/config/dns.php",
    "content": "<?php\n\nreturn [\n    'queue' => 'dns',\n\n    'nameservers' => env('DNS_NAMESERVERS', '1.1.1.1,1.0.0.1,9.9.9.9,8.8.8.8'),\n\n    'max_attempts' => (int) env('DNS_MAX_ATTEMPTS', 3),\n];\n"
  },
  {
    "path": "packages/dns/database/migrations/2024_07_16_073000_create_dns_monitors_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('dns_monitors', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(Site::class)->nullable()->constrained()->onDelete('cascade');\n            $table->foreignIdFor(Team::class)->constrained()->onDelete('cascade');\n\n            $table->string('type');\n            $table->string('record');\n            $table->string('value')->nullable();\n            $table->json('geoip')->nullable();\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('dns_monitors');\n    }\n};\n"
  },
  {
    "path": "packages/dns/database/migrations/2024_07_16_073500_create_dns_monitor_history_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('dns_monitor_histories', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(DnsMonitor::class)->constrained()->onDelete('cascade');\n            $table->foreignIdFor(Team::class)->constrained()->onDelete('cascade');\n\n            $table->string('type');\n            $table->string('value');\n            $table->json('geoip')->nullable();\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('dns_monitor_histories');\n    }\n};\n"
  },
  {
    "path": "packages/dns/database/migrations/2025_01_23_220000_dns_monitor_value_field_size.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('dns_monitors', function (Blueprint $table): void {\n            $table->string('value', 4096)->change();\n        });\n\n        Schema::table('dns_monitor_histories', function (Blueprint $table): void {\n            $table->string('value', 4096)->change();\n        });\n    }\n};\n"
  },
  {
    "path": "packages/dns/database/migrations/2025_02_01_180000_dns_monitor_enabled_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('dns_monitors', function (Blueprint $table): void {\n            $table->boolean('enabled')->default(true)->after('id');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropColumns('dns_monitors', ['enabled']);\n    }\n};\n"
  },
  {
    "path": "packages/dns/database/migrations/2025_03_22_090000_dns_monitor_value_field_nullable.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('dns_monitors', function (Blueprint $table): void {\n            $table->string('value', 4096)->nullable()->change();\n        });\n\n        Schema::table('dns_monitor_histories', function (Blueprint $table): void {\n            $table->string('value', 4096)->nullable()->change();\n        });\n    }\n};\n"
  },
  {
    "path": "packages/dns/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n    ignoreErrors:\n        - identifier: missingType.iterableValue\n        - identifier: missingType.generics\n"
  },
  {
    "path": "packages/dns/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/dns/resources/navigation.php",
    "content": "<?php\n\nuse Vigilant\\Core\\Facades\\Navigation;\n\nNavigation::add(route('dns.index'), 'DNS')\n    ->parent('infrastructure')\n    ->icon('phosphor-globe-simple')\n    ->gate('use-dns')\n    ->routeIs('dns*')\n    ->sort(400);\n"
  },
  {
    "path": "packages/dns/resources/views/components/empty-states/monitors.blade.php",
    "content": "<x-frontend::empty-state\n    :title=\"__('No DNS Monitors Yet')\"\n    :description=\"__('Add your domains to keep track of DNS records, changes, and resolution issues.')\"\n    icon=\"phosphor-warning-circle\"\n    iconClass=\"h-12 w-12 text-cyan\"\n    iconWrapperClass=\"rounded-full bg-cyan/10 p-4 mb-6\"\n    :buttonHref=\"route('dns.create')\"\n    :buttonText=\"__('Add DNS Monitor')\"\n    buttonClass=\"bg-cyan hover:bg-cyan/90 text-base-50 px-5 py-2.5 rounded-lg transition-all duration-300\"\n/>\n"
  },
  {
    "path": "packages/dns/resources/views/livewire/dns-monitor-form.blade.php",
    "content": "@props(['dnsMonitor' => null, 'inline' => false])\n<div>\n    <x-slot name=\"header\">\n        <x-page-header :title=\"$updating\n            ? __('Edit DNS Monitor - :type :record', [\n                'type' => $dnsMonitor->type->name,\n                'record' => $dnsMonitor->record,\n            ])\n            : __('Add DNS Monitor')\" :back=\"route('dns.index')\">\n        </x-page-header>\n    </x-slot>\n\n    <form wire:submit=\"save\">\n        <div class=\"max-w-7xl mx-auto\">\n            <x-card>\n                <div class=\"flex flex-col gap-4\">\n                    @if (!$inline)\n                        <x-form.checkbox field=\"form.enabled\" name=\"Enabled\" description=\"Enable or disable this DNS monitor\" />\n                    @endif\n\n                    <x-form.select field=\"form.type\" name=\"Type\" description=\"DNS Record Type\">\n                        @foreach (\\Vigilant\\Dns\\Enums\\Type::cases() as $type)\n                            <option value=\"{{ $type->value }}\">{{ $type->name }}</option>\n                        @endforeach\n                    </x-form.select>\n\n                    <x-form.text field=\"form.record\" name=\"Domain\" description=\"Domain to monitor\" />\n\n                    <x-form.text field=\"form.value\" name=\"Value\" description=\"Value that the record should resolve to\" />\n\n                    <div class=\"flex justify-end gap-4 items-center\">\n                        @if ($resolveFailed)\n                            <p class=\"text-red\">@lang('Failed to resolve record.')</p>\n                        @endif\n                        <x-form.button type=\"button\" class=\"bg-blue disabled:opacity-50\" wire:loading.attr=\"disabled\"\n                            wire:click=\"resolve\">@lang('Resolve value')</x-form.button>\n\n                        <x-form.submit-button dusk=\"submit-button\" wire:loading.attr=\"disabled\" :submitText=\"$updating ? 'Save' : 'Create'\" />\n                    </div>\n                </div>\n            </x-card>\n        </div>\n    </form>\n</div>\n"
  },
  {
    "path": "packages/dns/resources/views/livewire/import.blade.php",
    "content": "@props(['inline' => false])\n<div @class(['w-full', 'sm:px-6 lg:px-8' => !$inline])>\n    @if (!$inline)\n        <x-slot name=\"header\">\n            <x-page-header :title=\"__('Import domain')\" :back=\"route('dns.index')\">\n            </x-page-header>\n        </x-slot>\n    @endif\n\n    <div class=\"max-w-7xl mx-auto\">\n        <x-card>\n            <div class=\"flex flex-col gap-4\">\n                <div class=\"gap-4 grid grid-cols-2\">\n                    <div>\n                        <x-form.text field=\"domain\" name=\"Domain\" description=\"Domain to lookup DNS records for\" :live=\"false\" />\n                    </div>\n\n                    <div class=\"pt-2\">\n                        <x-form.button class=\"bg-gradient-to-r from-red via-orange to-red disabled:opacity-50\" wire:loading.attr=\"disabled\"\n                            type=\"button\" wire:click=\"lookup\">\n                            @lang('Import')\n                        </x-form.button>\n                    </div>\n                </div>\n\n                <div wire:loading>\n                    <span class=\"text-xs text-base-300\">@lang('Looking up DNS records')</span>\n                </div>\n                \n                @if ($records === [])\n                    @if ($noRecords)\n                        <p class=\"text-md text-base-200\">@lang('No records found')</p>\n                    @endif\n                @else\n                    <div class=\"inline-block min-w-full\" wire:loading.remove>\n                        <table class=\"min-w-full divide-y divide-base-400\">\n                            <thead>\n                                <tr>\n                                    <th class=\"py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-white sm:pl-0\">\n                                        @lang('Type')</th>\n                                    <th class=\"py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-white sm:pl-0\">\n                                        @lang('Host')</th>\n                                    <th class=\"py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-white sm:pl-0\">\n                                        @lang('Value')</th>\n                                    <th class=\"py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-white sm:pl-0\">\n                                        <span class=\"sr-only\">@lang('Remove')</span>\n                                    </th>\n                                </tr>\n                            </thead>\n                            <tbody class=\"divide-y divide-base-600\">\n                                @foreach ($records as $index => $record)\n                                    @continue(in_array($index, $deleted))\n\n                                    <tr class=\"hover:bg-base-800\">\n                                        <td class=\"whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-base-300 sm:pl-0\">\n                                            {{ $record['type']->name }}</td>\n                                        <td class=\"whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-base-300 sm:pl-0\">\n                                            {{ $record['host'] }}</td>\n                                        <td class=\"whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-base-300 sm:pl-0\">\n                                            {{ $record['value'] }}</td>\n                                        <td class=\"whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-base-300 sm:pl-0\">\n                                            <x-form.button class=\"bg-red\" type=\"button\"\n                                                wire:loading.attr=\"disabled\" wire:click=\"remove({{ $index }})\">\n                                                @lang('Remove')\n                                            </x-form.button>\n                                        </td>\n                                    </tr>\n                                @endforeach\n                            </tbody>\n                        </table>\n                    </div>\n\n                    @if (!$inline && count($records) > 0)\n                        <div class=\"flex justify-end\">\n                            <x-form.button class=\"bg-green hover:bg-green-light\" type=\"button\" wire:loading.attr=\"disabled\"\n                                wire:click=\"save()\">\n                                @lang('Save')\n                            </x-form.button>\n                        </div>\n                    @endif\n                @endif\n            </div>\n        </x-card>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/dns/resources/views/livewire/monitor/dashboard.blade.php",
    "content": "<dl class=\"grid grid-cols-4 gap-4\">\n    <x-frontend::stats-card :title=\"__('Monitored Records')\">\n        {{ $count }}\n    </x-frontend::stats-card>\n\n    <x-frontend::stats-card :title=\"__('Last DNS Change')\">\n        @if ($lastChange === null)\n            <x-frontend::mdash />\n        @else\n            @lang(':type record :record :time', [\n                'type' => $lastChange->monitor->type->value,\n                'record' => $lastChange->monitor->record,\n                'time' => $lastChange->created_at->diffForHumans(),\n            ])\n        @endif\n    </x-frontend::stats-card>\n</dl>\n"
  },
  {
    "path": "packages/dns/resources/views/livewire/monitor-history.blade.php",
    "content": "<div x-data=\"{ showDeleteModal: false }\">\n    <x-slot name=\"header\">\n        <x-page-header title=\"DNS History for {{ $monitor->type->name }} record: {{ $monitor->record }}\"\n            :back=\"route('dns.index')\">\n            <x-form.button class=\"bg-red\" type=\"button\" @click=\"$dispatch('open-delete-modal')\">\n                @lang('Delete')\n            </x-form.button>\n        </x-page-header>\n    </x-slot>\n\n    <livewire:dns-monitor-history-table :monitor=\"$monitor\" />\n\n    <!-- Delete Confirmation Modal -->\n    <div x-data=\"{ showDeleteModal: false }\" @open-delete-modal.window=\"showDeleteModal = true\">\n        <x-frontend::modal show=\"showDeleteModal\">\n            <x-frontend::modal.header icon=\"phosphor-trash\" iconColor=\"red\" show=\"showDeleteModal\">\n                @lang('Delete DNS Monitor')\n            </x-frontend::modal.header>\n\n            <x-frontend::modal.body>\n                <div class=\"space-y-4\">\n                    <p class=\"text-base-100\">\n                        @lang('Are you sure you want to delete this DNS monitor?')\n                    </p>\n                    <div class=\"bg-base-850 border border-base-700 rounded-lg p-4\">\n                        <div class=\"flex items-start gap-3\">\n                            <div class=\"flex-shrink-0\">\n                                @svg('phosphor-warning-circle', 'w-5 h-5 text-orange mt-0.5')\n                            </div>\n                            <div class=\"flex-1\">\n                                <p class=\"text-sm text-base-300\">\n                                    <span class=\"font-semibold text-base-100\">{{ $monitor->type->name }}</span> record for \n                                    <span class=\"font-semibold text-base-100\">{{ $monitor->record }}</span>\n                                </p>\n                                <p class=\"text-sm text-base-400 mt-1\">\n                                    @lang('This action cannot be undone. All history for this monitor will be permanently deleted.')\n                                </p>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </x-frontend::modal.body>\n\n            <x-frontend::modal.footer>\n                <x-form.button type=\"button\" @click=\"showDeleteModal = false\">\n                    @lang('Cancel')\n                </x-form.button>\n                <form action=\"{{ route('dns.delete', ['monitor' => $monitor]) }}\" method=\"POST\" class=\"inline\">\n                    @csrf\n                    @method('DELETE')\n                    <x-form.button class=\"bg-red\" type=\"submit\">\n                        @lang('Delete Monitor')\n                    </x-form.button>\n                </form>\n            </x-frontend::modal.footer>\n        </x-frontend::modal>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/dns/resources/views/livewire/monitors.blade.php",
    "content": "<div>\n    <x-slot name=\"header\">\n        <x-page-header title=\"DNS Monitoring\">\n            <x-frontend::page-header.actions>\n                <x-create-button dusk=\"dns-import-button\" :href=\"route('dns.import')\" model=\"Vigilant\\Dns\\Models\\DnsMonitor\">\n                    @lang('Import domain')\n                </x-create-button>\n                <x-create-button dusk=\"dns-add-button\" :href=\"route('dns.create')\" model=\"Vigilant\\Dns\\Models\\DnsMonitor\">\n                    @lang('Add DNS Monitor')\n                </x-create-button>\n            </x-frontend::page-header.actions>\n            <x-frontend::page-header.mobile-actions>\n                <x-create-button-dropdown dusk=\"dns-import-button\" :href=\"route('dns.import')\"\n                    model=\"Vigilant\\Dns\\Models\\DnsMonitor\" style=\"dropdown\">\n                    @lang('Import domain')\n                </x-create-button-dropdown>\n                <x-create-button-dropdown dusk=\"dns-add-button\" :href=\"route('dns.create')\" model=\"Vigilant\\Dns\\Models\\DnsMonitor\"\n                    style=\"dropdown\">\n                    @lang('Add DNS Monitor')\n                </x-create-button-dropdown>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    @if ($hasMonitors)\n        <livewire:dns-monitor-table />\n    @else\n        <x-dns::empty-states.monitors />\n    @endif\n\n</div>\n"
  },
  {
    "path": "packages/dns/routes/web.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\Dns\\Http\\Controllers\\DnsMonitorController;\nuse Vigilant\\Dns\\Livewire\\DnsImport;\nuse Vigilant\\Dns\\Livewire\\DnsMonitorForm;\nuse Vigilant\\Dns\\Livewire\\DnsMonitorHistory;\nuse Vigilant\\Dns\\Livewire\\DnsMonitors;\n\nRoute::prefix('dns')\n    ->middleware('can:use-dns')\n    ->group(function (): void {\n        Route::get('dns', DnsMonitors::class)->name('dns.index');\n        Route::get('dns/{monitor}/history', DnsMonitorHistory::class)->name('dns.history');\n        Route::delete('dns/{monitor}', [DnsMonitorController::class, 'delete'])->name('dns.delete')->can('delete,monitor');\n        Route::get('dns/create', DnsMonitorForm::class)->name('dns.create');\n        Route::get('dns/import', DnsImport::class)->name('dns.import');\n    });\n"
  },
  {
    "path": "packages/dns/src/Actions/CheckDnsRecord.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Actions;\n\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Dns\\Notifications\\RecordChangedNotification;\nuse Vigilant\\Dns\\Notifications\\RecordNotResolvedNotification;\n\nclass CheckDnsRecord\n{\n    public function __construct(\n        protected ResolveRecord $record,\n        protected TeamService $teamService\n    ) {}\n\n    public function check(DnsMonitor $monitor): void\n    {\n        $resolved = $this->record->resolve($monitor->type, $monitor->record);\n\n        if ($resolved === $monitor->value) {\n            return;\n        }\n\n        $this->teamService->setTeamById($monitor->team_id);\n\n        if ($resolved === null) {\n            $monitor->update([\n                'value' => null,\n            ]);\n\n            RecordNotResolvedNotification::notify($monitor, $monitor->history()->latest()->first());\n\n            return;\n        }\n\n        $previous = $monitor->history()->create([\n            'type' => $monitor->type,\n            'value' => $monitor->value,\n            'geoip' => $monitor->geoip,\n        ]);\n\n        $monitor->update([\n            'value' => $resolved,\n        ]);\n\n        RecordChangedNotification::notify($monitor, $previous);\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Actions/ResolveGeoIp.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Actions;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\n\nclass ResolveGeoIp\n{\n    public function resolve(DnsMonitor $monitor): void\n    {\n        $response = Http::get('http://ip-api.com/json/'.$monitor->value);\n\n        if (! $response->ok() || $response->json('status') !== 'success') {\n            return;\n        }\n\n        $geoip = $response->json();\n\n        $monitor->update([\n            'geoip' => [\n                'country_code' => $geoip['countryCode'],\n                'country_name' => $geoip['country'],\n                'region_code' => $geoip['region'],\n                'region_name' => $geoip['regionName'],\n                'city' => $geoip['city'],\n                'isp' => $geoip['isp'],\n                'org' => $geoip['org'],\n                'as' => $geoip['as'],\n            ],\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Actions/ResolveRecord.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Actions;\n\nuse BlueLibraries\\Dns\\Records\\AbstractRecord;\nuse Vigilant\\Dns\\Client\\DnsClient;\nuse Vigilant\\Dns\\Enums\\Type;\n\nclass ResolveRecord\n{\n    public function __construct(protected DnsClient $client) {}\n\n    public function resolve(Type $type, string $record): ?string\n    {\n        $result = collect($this->client->get($record, $type->flag()))\n            ->map(fn (AbstractRecord $record): array => $record->toArray())\n            ->toArray();\n\n        if (count($result) === 0) {\n            return null;\n        }\n\n        if (count($result) === 1) {\n            $result = $result[0];\n        }\n\n        $parser = $type->parser();\n\n        return $parser->parse($result);\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Client/DnsClient.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Client;\n\nuse BlueLibraries\\Dns\\DnsRecords;\nuse BlueLibraries\\Dns\\Handlers\\DnsHandlerException;\nuse BlueLibraries\\Dns\\Handlers\\Types\\TCP;\nuse BlueLibraries\\Dns\\Handlers\\Types\\UDP;\n\nclass DnsClient\n{\n    public function get(string $record, int|array $type, int $attempt = 0, bool $tcp = false): array\n    {\n        $maxAttempts = config()->integer('dns.max_attempts', 3);\n\n        if ($attempt > $maxAttempts) {\n            return [];\n        }\n\n        if ($attempt > 1) {\n            sleep($attempt); // Not the best solution, we should move this to DNS over HTTPs\n        }\n\n        $nameServer = $this->getNameserver();\n\n        if ($tcp) {\n            $dnsHandler = (new TCP)\n                ->setNameserver($nameServer)\n                ->setTimeout(3);\n        } else {\n            $dnsHandler = (new UDP)\n                ->setNameserver($nameServer)\n                ->setTimeout(3);\n        }\n\n        $dnsRecordsService = new DnsRecords($dnsHandler);\n\n        try {\n            $result = $dnsRecordsService->get($record, $type);\n        } catch (DnsHandlerException $e) {\n            logger()->error(\"Failed to retrieve DNS record $record on attempt $attempt with nameserer $nameServer: \".$e->getMessage().' '.$e->getTraceAsString());\n\n            return $this->get($record, $type, $attempt + 1, $attempt > 1);\n        }\n\n        if (count($result) === 0 && $attempt < $maxAttempts) {\n            return $this->get($record, $type, $attempt + 1, $attempt > 1);\n        }\n\n        return $result;\n    }\n\n    protected function getNameserver(): string\n    {\n        $nameservers = config()->string('dns.nameservers');\n\n        return str($nameservers)->explode(',')->random();\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Commands/CheckAllDnsRecordsCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Foundation\\Bus\\PendingDispatch;\nuse Vigilant\\Dns\\Jobs\\CheckDnsRecordJob;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\n\nclass CheckAllDnsRecordsCommand extends Command\n{\n    protected $signature = 'dns:check-all';\n\n    protected $description = 'Check All DNS Monitors';\n\n    public function handle(): int\n    {\n        DnsMonitor::query()\n            ->withoutGlobalScopes()\n            ->where('enabled', '=', true)\n            ->get()\n            ->each(fn (DnsMonitor $monitor): PendingDispatch => CheckDnsRecordJob::dispatch($monitor));\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Commands/CheckDnsRecordCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Dns\\Jobs\\CheckDnsRecordJob;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\n\nclass CheckDnsRecordCommand extends Command\n{\n    protected $signature = 'dns:check {recordId}';\n\n    protected $description = 'Check DNS Monitor';\n\n    public function handle(): int\n    {\n        /** @var int $recordId */\n        $recordId = $this->argument('recordId');\n\n        /** @var DnsMonitor $monitor */\n        $monitor = DnsMonitor::query()\n            ->withoutGlobalScopes()\n            ->findOrFail($recordId);\n\n        CheckDnsRecordJob::dispatch($monitor);\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Commands/ResolveGeoIpCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Vigilant\\Dns\\Enums\\Type;\nuse Vigilant\\Dns\\Jobs\\ResolveGeoIpJob;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\n\nclass ResolveGeoIpCommand extends Command\n{\n    protected $signature = 'dns:geoip {monitorId?}';\n\n    protected $description = 'Resolve GeoIP for monitor';\n\n    public function handle(): int\n    {\n        /** @var ?int $monitorId */\n        $monitorId = $this->argument('monitorId');\n\n        DnsMonitor::query()\n            ->withoutGlobalScopes()\n            ->when($monitorId !== null, fn (Builder $query) => $query->where('id', '=', $monitorId))\n            ->whereIn('type', Type::geoIpableTypes())\n            ->lazy()\n            ->each(fn (DnsMonitor $monitor) => ResolveGeoIpJob::dispatch($monitor));\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Enums/Type.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Enums;\n\nuse BlueLibraries\\Dns\\Records\\RecordTypes;\nuse Vigilant\\Dns\\RecordParsers\\RecordParser;\n\nenum Type: string\n{\n    case A = 'A';\n    case AAAA = 'AAAA';\n    case CNAME = 'CNAME';\n    case MX = 'MX';\n    case NS = 'NS';\n    case PTR = 'PTR';\n    case SOA = 'SOA';\n    case SRV = 'SRV';\n    case TXT = 'TXT';\n    case NAPTR = 'NAPTR';\n    case CAA = 'CAA';\n\n    public function flag(): int\n    {\n        return match ($this) {\n            self::A => RecordTypes::A,\n            self::AAAA => RecordTypes::AAAA,\n            self::CNAME => RecordTypes::CNAME,\n            self::MX => RecordTypes::MX,\n            self::NS => RecordTypes::NS,\n            self::PTR => RecordTypes::PTR,\n            self::SOA => RecordTypes::SOA,\n            self::SRV => RecordTypes::SRV,\n            self::TXT => RecordTypes::TXT,\n            self::NAPTR => RecordTypes::NAPTR,\n            self::CAA => RecordTypes::CAA,\n        };\n    }\n\n    public function hasParser(): bool\n    {\n        return class_exists('\\Vigilant\\Dns\\RecordParsers\\\\'.$this->name);\n    }\n\n    public function parser(): RecordParser\n    {\n        $class = '\\Vigilant\\Dns\\RecordParsers\\\\'.$this->name;\n\n        throw_if(! class_exists($class), 'No parser for type '.$this->name);\n\n        /** @var RecordParser $instance */\n        $instance = app($class);\n\n        return $instance;\n    }\n\n    public static function geoIpableTypes(): array\n    {\n        return [\n            Type::A,\n            Type::AAAA,\n            Type::CNAME,\n            Type::MX,\n            Type::PTR,\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Http/Controllers/DnsMonitorController.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Http\\Controllers;\n\nuse Illuminate\\Routing\\Controller;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\n\nclass DnsMonitorController extends Controller\n{\n    use DisplaysAlerts;\n\n    public function delete(DnsMonitor $monitor): mixed\n    {\n        $monitor->delete();\n\n        $this->alert(\n            __('Deleted'),\n            __('DNS monitor was successfully deleted'),\n            AlertType::Success\n        );\n\n        return response()->redirectToRoute('dns.index');\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Jobs/CheckDnsRecordJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Dns\\Actions\\CheckDnsRecord;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\n\nclass CheckDnsRecordJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(\n        public DnsMonitor $monitor\n    ) {\n        $this->onQueue(config('dns.queue'));\n    }\n\n    public function handle(CheckDnsRecord $record): void\n    {\n        $record->check($this->monitor);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->monitor->id;\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Jobs/ResolveGeoIpJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Dns\\Actions\\ResolveGeoIp;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\n\nclass ResolveGeoIpJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(\n        public DnsMonitor $monitor\n    ) {\n        $this->onQueue(config('dns.queue'));\n    }\n\n    public function handle(ResolveGeoIp $geoIp): void\n    {\n        $geoIp->resolve($this->monitor);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->monitor->id;\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Livewire/DnsImport.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Livewire;\n\nuse BlueLibraries\\Dns\\Records\\AbstractRecord;\nuse BlueLibraries\\Dns\\Records\\RecordTypes;\nuse Illuminate\\Contracts\\View\\View;\nuse Illuminate\\Support\\Str;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\Dns\\Client\\DnsClient;\nuse Vigilant\\Dns\\Enums\\Type;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Frontend\\Traits\\CanBeInline;\nuse Vigilant\\Frontend\\Validation\\Fqdn;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass DnsImport extends Component\n{\n    use CanBeInline;\n    use DisplaysAlerts;\n\n    #[Locked]\n    public ?int $siteId = null;\n\n    public string $domain = '';\n\n    public array $records = [];\n\n    public array $deleted = [];\n\n    public bool $noRecords = false;\n\n    public function mount(?int $siteId = null): void\n    {\n        $this->siteId = $siteId;\n\n        if ($siteId !== null) {\n            /** @var Site $site */\n            $site = Site::query()->findOrFail($siteId);\n\n            $this->records = $site->dnsMonitors->map(function (DnsMonitor $monitor): array {\n                return [\n                    'monitor_id' => $monitor->id,\n                    'type' => $monitor->type,\n                    'host' => $monitor->record,\n                    'value' => $monitor->value,\n                ];\n            })->toArray();\n\n            $this->domain = Str::of($site->url)->replace(['https://', 'http://'], '')->before('/')->value();\n        }\n    }\n\n    public function remove(int $index): void\n    {\n        $this->deleted[] = $index;\n        if (count($this->deleted) === count($this->records)) {\n            $this->records = [];\n        }\n    }\n\n    #[On('save')]\n    public function save(): void\n    {\n        $this->authorize('create', DnsMonitor::class);\n\n        foreach ($this->records as $index => $record) {\n            if (in_array($index, $this->deleted)) {\n                if (array_key_exists('monitor_id', $record)) {\n                    DnsMonitor::query()\n                        ->where('id', '=', $record['monitor_id'])\n                        ->delete();\n                }\n\n                continue;\n            }\n\n            DnsMonitor::query()->updateOrCreate([\n                'site_id' => $this->siteId,\n                'type' => $record['type'],\n                'record' => $record['host'],\n            ], [\n                'value' => $record['value'],\n            ]);\n        }\n\n        if ($this->inline) {\n            return;\n        }\n\n        $this->alert(\n            __('Saved'),\n            __('Selected records are being monitored'),\n            AlertType::Success\n        );\n        $this->redirectRoute('dns.index');\n    }\n\n    public function lookup(): void\n    {\n        $this->records = [];\n\n        $this->validate([\n            'domain' => ['required', 'max:255', new Fqdn],\n        ]);\n\n        /** @var DnsClient $client */\n        $client = app(DnsClient::class);\n\n        /** @var array<int, AbstractRecord> $records */\n        $records = $client->get($this->domain, [\n            RecordTypes::A,\n            RecordTypes::AAAA,\n            RecordTypes::CNAME,\n            RecordTypes::SOA,\n            RecordTypes::TXT,\n            RecordTypes::MX,\n            RecordTypes::NS,\n        ]);\n\n        foreach ($records as $record) {\n            $data = $record->toArray();\n\n            $type = Type::tryFrom($data['type']);\n\n            if ($type === null) {\n                continue;\n            }\n\n            $value = $type->parser()->parse($data);\n\n            $this->records[] = [\n                'type' => $type,\n                'host' => $data['host'],\n                'value' => $value,\n            ];\n        }\n\n        $this->noRecords = count($records) === 0;\n        $this->deleted = [];\n    }\n\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'dns::livewire.import';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Livewire/DnsMonitorForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Livewire;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\nuse Vigilant\\Dns\\Actions\\ResolveRecord;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\n\nclass DnsMonitorForm extends Component\n{\n    use DisplaysAlerts;\n\n    public Forms\\DnsMonitorForm $form;\n\n    public bool $resolveFailed = false;\n\n    #[Locked]\n    public DnsMonitor $dnsMonitor;\n\n    public function mount(?DnsMonitor $monitor): void\n    {\n        if ($monitor !== null) {\n            if ($monitor->exists) {\n                $this->authorize('update', $monitor);\n            } else {\n                $this->authorize('create', $monitor);\n            }\n\n            $this->form->fill($monitor->toArray());\n            $this->dnsMonitor = $monitor;\n        }\n    }\n\n    public function resolve(): void\n    {\n        $this->validate([\n            'form.record' => 'required',\n            'form.type' => 'required',\n        ]);\n\n        /** @var ResolveRecord $resolver */\n        $resolver = app(ResolveRecord::class);\n\n        $result = $resolver->resolve($this->form->type, $this->form->record);\n\n        if ($result !== null) {\n            $this->form->value = $result;\n            $this->resolveFailed = false;\n        } else {\n            $this->resolveFailed = true;\n        }\n    }\n\n    public function save(): void\n    {\n        $this->validate();\n\n        if ($this->dnsMonitor->exists) {\n            $this->authorize('update', $this->dnsMonitor);\n\n            $this->dnsMonitor->update($this->form->all());\n        } else {\n            $this->authorize('create', $this->dnsMonitor);\n\n            $exists = DnsMonitor::query()\n                ->where('record', '=', $this->form->record)\n                ->where('type', '=', $this->form->type)\n                ->exists();\n\n            if ($exists) {\n                $this->addError('form.record', __('DNS monitor with this record and type already exists'));\n\n                return;\n            }\n\n            $this->dnsMonitor = DnsMonitor::query()->create(\n                $this->form->all()\n            );\n        }\n\n        $this->alert(\n            __('Saved'),\n            __('DNS monitor was successfully :action',\n                ['action' => $this->dnsMonitor->wasRecentlyCreated ? 'created' : 'saved']),\n            AlertType::Success\n        );\n        $this->redirectRoute('dns.index');\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'dns::livewire.dns-monitor-form';\n\n        return view($view, [\n            'updating' => $this->dnsMonitor->exists,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Livewire/DnsMonitorHistory.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Livewire;\n\nuse Illuminate\\Contracts\\View\\View;\nuse Livewire\\Component;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\n\nclass DnsMonitorHistory extends Component\n{\n    public DnsMonitor $monitor;\n\n    public function mount(DnsMonitor $monitor): void\n    {\n        $this->monitor = $monitor;\n    }\n\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'dns::livewire.monitor-history';\n\n        return view($view, [\n            'monitor' => $this->monitor,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Livewire/DnsMonitors.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Livewire;\n\nuse Illuminate\\Contracts\\View\\View;\nuse Livewire\\Component;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\n\nclass DnsMonitors extends Component\n{\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'dns::livewire.monitors';\n        $hasMonitors = DnsMonitor::query()->exists();\n\n        return view($view, [\n            'hasMonitors' => $hasMonitors,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Livewire/Forms/DnsMonitorForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Livewire\\Forms;\n\nuse Illuminate\\Validation\\Rule;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Form;\nuse Vigilant\\Core\\Validation\\CanEnableRule;\nuse Vigilant\\Dns\\Enums\\Type;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Frontend\\Validation\\Fqdn;\n\nclass DnsMonitorForm extends Form\n{\n    #[Locked]\n    public ?int $site_id;\n\n    public bool $enabled = true;\n\n    public Type $type = Type::A;\n\n    public string $record = '';\n\n    public string $value = '';\n\n    public function rules(): array\n    {\n        return [\n            'type' => [\n                'required',\n                Rule::enum(Type::class),\n            ],\n            'record' => ['required', 'max:255', new Fqdn],\n            'value' => ['nullable', 'max:255'],\n            'enabled' => ['boolean', new CanEnableRule(DnsMonitor::class)],\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Livewire/Monitor/Dashboard.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Livewire\\Monitor;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Dns\\Models\\DnsMonitorHistory;\n\nclass Dashboard extends Component\n{\n    #[Locked]\n    public int $siteId;\n\n    public function mount(int $siteId): void\n    {\n        $this->siteId = $siteId;\n    }\n\n    public function render(): mixed\n    {\n        $dnsMonitors = DnsMonitor::query()\n            ->where('site_id', $this->siteId)\n            ->get();\n\n        $latestChange = DnsMonitorHistory::query()\n            ->whereIn('dns_monitor_id', $dnsMonitors->pluck('id'))\n            ->orderByDesc('created_at')\n            ->first();\n\n        /** @var view-string $view */\n        $view = 'dns::livewire.monitor.dashboard';\n\n        return view($view, [\n            'count' => $dnsMonitors->count(),\n            'lastChange' => $latestChange,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Livewire/Tables/DnsMonitorHistoryTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Livewire\\Attributes\\Locked;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Dns\\Models\\DnsMonitorHistory;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\HoverColumn;\n\nclass DnsMonitorHistoryTable extends BaseTable\n{\n    protected string $model = DnsMonitorHistory::class;\n\n    #[Locked]\n    public DnsMonitor $monitor;\n\n    public string $sortColumn = 'created_at';\n\n    public string $sortDirection = 'desc';\n\n    public function mount(DnsMonitor $monitor): void\n    {\n        $this->monitor = $monitor;\n    }\n\n    protected function columns(): array\n    {\n        return [\n            Column::make(__('Type'), 'type')\n                ->searchable()\n                ->sortable(),\n\n            HoverColumn::make(__('Value'), 'value')\n                ->searchable()\n                ->sortable(),\n\n            Column::make(__('Modified At'), 'created_at')\n                ->searchable()\n                ->sortable(),\n        ];\n    }\n\n    protected function query(): Builder\n    {\n        return parent::query()->where('dns_monitor_id', '=', $this->monitor->id);\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Livewire/Tables/DnsMonitorTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Enumerable;\nuse Illuminate\\Support\\Facades\\Gate;\nuse RamonRietdijk\\LivewireTables\\Actions\\Action;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse RamonRietdijk\\LivewireTables\\Filters\\SelectFilter;\nuse Vigilant\\Dns\\Jobs\\CheckDnsRecordJob;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\Enums\\Status;\nuse Vigilant\\Frontend\\Integrations\\Table\\GeoIpColumn;\nuse Vigilant\\Frontend\\Integrations\\Table\\HoverColumn;\nuse Vigilant\\Frontend\\Integrations\\Table\\StatusColumn;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass DnsMonitorTable extends BaseTable\n{\n    protected string $model = DnsMonitor::class;\n\n    protected function columns(): array\n    {\n        return [\n            StatusColumn::make(__('Status'))\n                ->text(function (DnsMonitor $monitor): string {\n                    return $monitor->enabled ? __('Enabled') : __('Disabled');\n                })\n                ->status(function (DnsMonitor $monitor): Status {\n                    return $monitor->enabled ? Status::Success : Status::Danger;\n                }),\n\n            Column::make(__('Type'), 'type')\n                ->searchable()\n                ->sortable(),\n\n            Column::make(__('Record'), 'record')\n                ->searchable()\n                ->sortable(),\n\n            HoverColumn::make(__('Value'), 'value')\n                ->searchable()\n                ->sortable(),\n\n            Column::make(__('Last modified'), fn (DnsMonitor $monitor): string => $monitor->lastHistory()?->created_at?->toDateString() ?? '-'),\n\n            GeoIpColumn::make(__('Location'), 'geoip.country_code'),\n        ];\n    }\n\n    protected function link(Model $record): string\n    {\n        return route('dns.history', ['monitor' => $record]);\n    }\n\n    protected function filters(): array\n    {\n        return [\n            SelectFilter::make(__('Site'), 'site_id')\n                ->options(\n                    Site::query()\n                        ->orderBy('url')\n                        ->pluck('url', 'id')\n                        ->toArray()\n                ),\n        ];\n    }\n\n    protected function actions(): array\n    {\n        return [\n            Action::make(__('Check'), function (Enumerable $models): void {\n                $models->each(fn (DnsMonitor $monitor) => CheckDnsRecordJob::dispatch($monitor));\n            }, 'check'),\n\n            Action::make(__('Enable'), function (Enumerable $models): void {\n                foreach ($models as $model) {\n                    if (! Gate::allows('create', $model)) {\n                        break;\n                    }\n\n                    $model->update(['enabled' => true]);\n                }\n            }, 'enable'),\n\n            Action::make(__('Disable'), function (Enumerable $models): void {\n                $models->each(fn (DnsMonitor $monitor) => $monitor->update(['enabled' => false]));\n            }, 'disable'),\n\n            Action::make(__('Delete'), function (Enumerable $models): void {\n                $models->each(fn (DnsMonitor $monitor) => $monitor->delete());\n            }, 'delete'),\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Models/DnsMonitor.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Dns\\Enums\\Type;\nuse Vigilant\\Dns\\Observers\\GeoipObserver;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Observers\\TeamObserver;\n\n/**\n * @property int $id\n * @property bool $enabled\n * @property ?int $site_id\n * @property int $team_id\n * @property Type $type\n * @property string $record\n * @property ?string $value\n * @property ?array $geoip\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Site $site\n * @property Collection<int, DnsMonitorHistory> $history\n */\n#[ObservedBy([TeamObserver::class, GeoipObserver::class])]\n#[ScopedBy(TeamScope::class)]\nclass DnsMonitor extends Model\n{\n    protected $guarded = [];\n\n    protected $casts = [\n        'enabled' => 'bool',\n        'type' => Type::class,\n        'geoip' => 'array',\n    ];\n\n    public function site(): BelongsTo\n    {\n        return $this->belongsTo(Site::class);\n    }\n\n    public function history(): HasMany\n    {\n        return $this->hasMany(DnsMonitorHistory::class);\n    }\n\n    public function lastHistory(): ?DnsMonitorHistory\n    {\n        /** @var ?DnsMonitorHistory $history */\n        $history = $this->history()->orderByDesc('id')->first();\n\n        return $history;\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Models/DnsMonitorHistory.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Prunable;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Concerns\\HasDataRetention;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Dns\\Enums\\Type;\nuse Vigilant\\Users\\Observers\\TeamObserver;\n\n/**\n * @property int $id\n * @property int $dns_monitor_id\n * @property int $team_id\n * @property Type $type\n * @property string $value\n * @property ?array $geoip\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?DnsMonitor $monitor\n */\n#[ObservedBy(TeamObserver::class)]\n#[ScopedBy(TeamScope::class)]\nclass DnsMonitorHistory extends Model\n{\n    use HasDataRetention;\n    use Prunable;\n\n    protected $guarded = [];\n\n    protected $casts = [\n        'type' => Type::class,\n        'geoip' => 'array',\n    ];\n\n    public function monitor(): BelongsTo\n    {\n        return $this->belongsTo(DnsMonitor::class, 'dns_monitor_id');\n    }\n\n    public function prunable(): Builder\n    {\n        return static::withoutGlobalScopes()->where('created_at', '<=', $this->retentionPeriod());\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Notifications/Conditions/RecordTypeCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Notifications\\Conditions;\n\nuse Vigilant\\Dns\\Enums\\Type;\nuse Vigilant\\Dns\\Notifications\\RecordChangedNotification;\nuse Vigilant\\Dns\\Notifications\\RecordNotResolvedNotification;\nuse Vigilant\\Notifications\\Conditions\\SelectCondition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass RecordTypeCondition extends SelectCondition\n{\n    public static string $name = 'Record Type';\n\n    public function options(): array\n    {\n        return collect(Type::cases())\n            ->mapWithKeys(fn (Type $type): array => [$type->value => $type->name])\n            ->toArray();\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'is',\n            '!=' => 'is not',\n        ];\n    }\n\n    public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool\n    {\n        /** @var RecordChangedNotification|RecordNotResolvedNotification $notification */\n        $selectedType = Type::tryFrom($value);\n\n        if ($selectedType === null) {\n            return false;\n        }\n\n        $type = $notification->monitor->type;\n\n        return match ($operator) {\n            '=' => $type == $selectedType,\n            '!=' => $type != $selectedType,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Notifications/RecordChangedNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Notifications;\n\nuse Vigilant\\Dns\\Enums\\Type;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Dns\\Models\\DnsMonitorHistory;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass RecordChangedNotification extends Notification implements HasSite\n{\n    public static string $name = 'DNS Record Changed';\n\n    public Level $level = Level::Warning;\n\n    public function __construct(public DnsMonitor $monitor, public DnsMonitorHistory $previous) {}\n\n    public function title(): string\n    {\n        return __('DNS Record :type :record has been changed', ['type' => $this->monitor->type->name, 'record' => $this->monitor->record]);\n    }\n\n    public function description(): string\n    {\n        return __('The :type record for :record has been changed from :old to :new at :changedate', [\n            'type' => $this->monitor->type->name,\n            'record' => $this->monitor->record,\n            'old' => $this->previous->value ?? '?',\n            'new' => $this->monitor->value ?? '?',\n            'changedate' => $this->previous->created_at?->toDateString() ?? '?',\n        ]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when a DNS record value changes.');\n    }\n\n    public function level(): Level\n    {\n        $critical = [\n            Type::A,\n            Type::AAAA,\n            Type::NS,\n            Type::MX,\n        ];\n\n        return in_array($this->monitor->type, $critical)\n            ? Level::Critical\n            : Level::Warning;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->monitor->site;\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->monitor->id;\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Notifications/RecordNotResolvedNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Notifications;\n\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Dns\\Models\\DnsMonitorHistory;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass RecordNotResolvedNotification extends Notification implements HasSite\n{\n    public static string $name = 'Unable to resolve DNS record';\n\n    public Level $level = Level::Critical;\n\n    public function __construct(public DnsMonitor $monitor, public ?DnsMonitorHistory $previous) {}\n\n    public function title(): string\n    {\n        return __('Unable to resolve DNS record :record', ['record' => $this->monitor->record]);\n    }\n\n    public function description(): string\n    {\n        return __('The DNS record for :record was not resolved. The previous value was :old', [\n            'old' => $this->previous->value ?? 'None',\n            'record' => $this->monitor->record,\n        ]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when a DNS record fails to resolve or becomes unavailable.');\n    }\n\n    public function site(): ?Site\n    {\n        return $this->monitor->site;\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->monitor->id;\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/Observers/GeoipObserver.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Observers;\n\nuse Vigilant\\Dns\\Enums\\Type;\nuse Vigilant\\Dns\\Jobs\\ResolveGeoIpJob;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\n\nclass GeoipObserver\n{\n    public function updating(DnsMonitor $monitor): void\n    {\n        if ($monitor->isDirty('value') && in_array($monitor->type, Type::geoIpableTypes())) {\n            ResolveGeoIpJob::dispatch($monitor)->delay(now()->addSeconds(5));\n        }\n    }\n\n    public function created(DnsMonitor $monitor): void\n    {\n        if (in_array($monitor->type, Type::geoIpableTypes())) {\n            ResolveGeoIpJob::dispatch($monitor);\n        }\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/RecordParsers/A.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\RecordParsers;\n\nclass A extends RecordParser\n{\n    public string $field = 'ip';\n}\n"
  },
  {
    "path": "packages/dns/src/RecordParsers/AAAA.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\RecordParsers;\n\nclass AAAA extends RecordParser\n{\n    public string $field = 'ipv6';\n}\n"
  },
  {
    "path": "packages/dns/src/RecordParsers/CAA.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\RecordParsers;\n\nclass CAA extends RecordParser\n{\n    public string $field = 'value';\n}\n"
  },
  {
    "path": "packages/dns/src/RecordParsers/CNAME.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\RecordParsers;\n\nclass CNAME extends RecordParser\n{\n    public string $field = 'target';\n}\n"
  },
  {
    "path": "packages/dns/src/RecordParsers/MX.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\RecordParsers;\n\nclass MX extends RecordParser\n{\n    public string $field = 'target';\n}\n"
  },
  {
    "path": "packages/dns/src/RecordParsers/NS.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\RecordParsers;\n\nclass NS extends RecordParser\n{\n    public string $field = 'target';\n}\n"
  },
  {
    "path": "packages/dns/src/RecordParsers/RecordParser.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\RecordParsers;\n\nabstract class RecordParser\n{\n    public string $field = '';\n\n    public function parse(array $result): ?string\n    {\n        if (array_key_exists($this->field, $result)) {\n            return $result[$this->field];\n        }\n\n        $values = collect($result)->pluck($this->field);\n\n        return $values->isEmpty()\n            ? null\n            : $values->implode(',');\n    }\n}\n"
  },
  {
    "path": "packages/dns/src/RecordParsers/SOA.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\RecordParsers;\n\nclass SOA extends RecordParser\n{\n    public string $field = 'mname';\n}\n"
  },
  {
    "path": "packages/dns/src/RecordParsers/TXT.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\RecordParsers;\n\nclass TXT extends RecordParser\n{\n    public string $field = 'txt';\n}\n"
  },
  {
    "path": "packages/dns/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns;\n\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\nuse Livewire\\Livewire;\nuse Vigilant\\Core\\Facades\\Navigation;\nuse Vigilant\\Core\\Policies\\AllowAllPolicy;\nuse Vigilant\\Dns\\Commands\\CheckAllDnsRecordsCommand;\nuse Vigilant\\Dns\\Commands\\CheckDnsRecordCommand;\nuse Vigilant\\Dns\\Commands\\ResolveGeoIpCommand;\nuse Vigilant\\Dns\\Livewire\\DnsImport;\nuse Vigilant\\Dns\\Livewire\\DnsMonitorForm;\nuse Vigilant\\Dns\\Livewire\\DnsMonitors;\nuse Vigilant\\Dns\\Livewire\\Monitor\\Dashboard;\nuse Vigilant\\Dns\\Livewire\\Tables\\DnsMonitorHistoryTable;\nuse Vigilant\\Dns\\Livewire\\Tables\\DnsMonitorTable;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Dns\\Notifications\\Conditions\\RecordTypeCondition;\nuse Vigilant\\Dns\\Notifications\\RecordChangedNotification;\nuse Vigilant\\Dns\\Notifications\\RecordNotResolvedNotification;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Sites\\Conditions\\SiteCondition;\nuse Vigilant\\Users\\Models\\User;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function register(): void\n    {\n        $this\n            ->registerConfig();\n    }\n\n    protected function registerConfig(): static\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/dns.php', 'dns');\n\n        return $this;\n    }\n\n    public function boot(): void\n    {\n        $this\n            ->bootConfig()\n            ->bootMigrations()\n            ->bootCommands()\n            ->bootViews()\n            ->bootLivewire()\n            ->bootRoutes()\n            ->bootNavigation()\n            ->bootNotifications()\n            ->bootGates()\n            ->bootPolicies();\n    }\n\n    protected function bootConfig(): static\n    {\n        $this->publishes([\n            __DIR__.'/../config/dns.php' => config_path('dns.php'),\n        ], 'config');\n\n        return $this;\n    }\n\n    protected function bootMigrations(): static\n    {\n        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');\n\n        return $this;\n    }\n\n    protected function bootCommands(): static\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n                CheckDnsRecordCommand::class,\n                CheckAllDnsRecordsCommand::class,\n                ResolveGeoIpCommand::class,\n            ]);\n        }\n\n        return $this;\n    }\n\n    protected function bootViews(): static\n    {\n        $this->loadViewsFrom(__DIR__.'/../resources/views', 'dns');\n\n        return $this;\n    }\n\n    protected function bootLivewire(): static\n    {\n        Livewire::component('dns-monitors', DnsMonitors::class);\n        Livewire::component('dns-monitor-form', DnsMonitorForm::class);\n        Livewire::component('dns-monitor-table', DnsMonitorTable::class);\n        Livewire::component('dns-monitor-history-table', DnsMonitorHistoryTable::class);\n        Livewire::component('dns-monitor-import', DnsImport::class);\n        Livewire::component('dns-monitor-dashboard', Dashboard::class);\n\n        return $this;\n    }\n\n    protected function bootRoutes(): static\n    {\n        if (! $this->app->routesAreCached()) {\n            Route::middleware(['web', 'auth'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/web.php'));\n        }\n\n        return $this;\n    }\n\n    protected function bootNavigation(): static\n    {\n        Navigation::path(__DIR__.'/../resources/navigation.php');\n\n        return $this;\n    }\n\n    protected function bootNotifications(): static\n    {\n        NotificationRegistry::registerNotification([\n            RecordChangedNotification::class,\n            RecordNotResolvedNotification::class,\n        ]);\n\n        NotificationRegistry::registerCondition(RecordChangedNotification::class, [\n            SiteCondition::class,\n            RecordTypeCondition::class,\n        ]);\n\n        NotificationRegistry::registerCondition(RecordNotResolvedNotification::class, [\n            SiteCondition::class,\n            RecordTypeCondition::class,\n        ]);\n\n        return $this;\n    }\n\n    protected function bootGates(): static\n    {\n        Gate::define('use-dns', function (User $user): bool {\n            return ce();\n        });\n\n        return $this;\n    }\n\n    protected function bootPolicies(): static\n    {\n        if (ce()) {\n            Gate::policy(DnsMonitor::class, AllowAllPolicy::class);\n        }\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/dns/testbench.yaml",
    "content": "providers:\n  - Vigilant\\Dns\\ServiceProvider\n"
  },
  {
    "path": "packages/dns/tests/Actions/CheckDnsRecordTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Tests\\Actions;\n\nuse Mockery\\MockInterface;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Dns\\Actions\\CheckDnsRecord;\nuse Vigilant\\Dns\\Actions\\ResolveRecord;\nuse Vigilant\\Dns\\Enums\\Type;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Dns\\Notifications\\RecordChangedNotification;\nuse Vigilant\\Dns\\Tests\\TestCase;\n\nclass CheckDnsRecordTest extends TestCase\n{\n    #[Test]\n    public function it_does_not_update_when_value_is_unchanged(): void\n    {\n        RecordChangedNotification::fake();\n\n        $this->mock(ResolveRecord::class, function (MockInterface $mock): void {\n            $mock->shouldReceive('resolve')->with(Type::A, 'govigilant.io')->andReturn('127.0.0.1');\n        });\n\n        $monitor = DnsMonitor::query()->create([\n            'type' => Type::A,\n            'record' => 'govigilant.io',\n            'value' => '127.0.0.1',\n        ]);\n\n        /** @var CheckDnsRecord $action */\n        $action = app(CheckDnsRecord::class);\n\n        $action->check($monitor);\n\n        $this->assertFalse(RecordChangedNotification::wasDispatched());\n    }\n\n    #[Test]\n    public function it_handles_change(): void\n    {\n        RecordChangedNotification::fake();\n\n        $this->mock(ResolveRecord::class, function (MockInterface $mock): void {\n            $mock->shouldReceive('resolve')->with(Type::A, 'govigilant.io')->andReturn('127.0.0.2');\n        });\n\n        $monitor = DnsMonitor::query()->create([\n            'type' => Type::A,\n            'record' => 'govigilant.io',\n            'value' => '127.0.0.1',\n        ]);\n\n        /** @var CheckDnsRecord $action */\n        $action = app(CheckDnsRecord::class);\n\n        $action->check($monitor);\n\n        $this->assertTrue(RecordChangedNotification::wasDispatched());\n        $this->assertEquals(1, $monitor->history->count());\n        $this->assertEquals('127.0.0.1', $monitor->history->first()?->value);\n    }\n}\n"
  },
  {
    "path": "packages/dns/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\Dns\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Livewire\\LivewireServiceProvider;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Dns\\ServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        TeamService::fake();\n    }\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n            \\Vigilant\\Users\\ServiceProvider::class,\n            LivewireServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/frontend/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/frontend/composer.json",
    "content": "{\n    \"name\": \"vigilant/frontend\",\n    \"description\": \"Vigilant Frontend - Collection of frontend components\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"laravel/framework\": \"^12.0\",\n        \"league/iso3166\": \"^4.3\",\n        \"livewire/livewire\": \"^3.4\",\n        \"outhebox/blade-flags\": \"^1.5\",\n        \"ramonrietdijk/livewire-tables\": \"^6.0\",\n        \"vigilant/core\": \"@dev\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Frontend\\\\\": \"src\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Frontend\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\Frontend\\\\ServiceProvider\"\n            ]\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/frontend/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n    ignoreErrors:\n        - identifier: missingType.iterableValue\n        - identifier: trait.unused\n"
  },
  {
    "path": "packages/frontend/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/frontend/resources/views/components/card.blade.php",
    "content": "@props(['padding' => true])\n\n<div {{ $attributes->merge(['class' => 'border border-base-700 shadow-xl rounded-xl overflow-hidden backdrop-blur-sm relative ' . ($padding ? 'px-6 py-8 sm:p-8' : '')]) }}>\n    <!-- Multi-stop gradient background to prevent banding -->\n    <div class=\"absolute inset-0 -z-10\" \n         style=\"background: \n                linear-gradient(135deg, \n                    rgba(35, 35, 51, 1) 0%, \n                    rgba(33, 33, 48, 1) 10%,\n                    rgba(31, 31, 45, 1) 20%,\n                    rgba(29, 29, 42, 1) 30%,\n                    rgba(28, 28, 40, 1) 40%,\n                    rgba(27, 27, 38, 1) 50%,\n                    rgba(26, 26, 36, 1) 60%,\n                    rgba(26, 26, 36, 1) 70%,\n                    rgba(26, 26, 36, 1) 80%,\n                    rgba(26, 26, 36, 1) 90%,\n                    rgba(26, 26, 36, 1) 100%\n                ),\n                url('data:image/svg+xml,%3Csvg viewBox=%220 0 400 400%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noiseFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%221.5%22 numOctaves=%225%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noiseFilter)%22 opacity=%220.12%22/%3E%3C/svg%3E');\">\n    </div>\n    \n    <!-- Subtle gradient overlay for depth with noise -->\n    <div class=\"absolute inset-0 pointer-events-none\"\n         style=\"background: \n                linear-gradient(180deg, \n                    rgba(45, 45, 66, 0.1) 0%, \n                    rgba(45, 45, 66, 0.075) 10%,\n                    rgba(45, 45, 66, 0.05) 20%,\n                    rgba(45, 45, 66, 0.025) 30%,\n                    rgba(45, 45, 66, 0.01) 40%,\n                    transparent 50%\n                ),\n                url('data:image/svg+xml,%3Csvg viewBox=%220 0 300 300%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22grainFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%221%22 numOctaves=%224%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23grainFilter)%22 opacity=%220.08%22/%3E%3C/svg%3E');\"></div>\n    \n    <div class=\"relative\">\n        {{ $slot }}\n    </div>\n</div>\n\n"
  },
  {
    "path": "packages/frontend/resources/views/components/empty-state.blade.php",
    "content": "@props([\n    'title',\n    'description',\n    'icon' => 'phosphor-warning-circle',\n    'iconClass' => 'h-12 w-12 text-base-100',\n    'iconWrapperClass' => 'rounded-full bg-base-700/50 p-4 mb-6',\n    'buttonHref' => null,\n    'buttonText' => null,\n    'buttonClass' => 'bg-red text-base-50 px-5 py-2.5 rounded-lg transition-all duration-300',\n    'wrapperClass' => 'mx-auto max-w-3xl text-center py-12',\n    'cardClass' => 'bg-base-850/50 border-base-700/50',\n    'contentClass' => 'flex flex-col items-center',\n    'titleClass' => 'text-2xl font-bold text-base-50 mb-2',\n    'descriptionClass' => 'text-base text-base-300 mb-8 max-w-md',\n])\n\n<div class=\"{{ $wrapperClass }}\">\n    <x-card class=\"{{ $cardClass }}\">\n        <div class=\"{{ $contentClass }}\">\n            @if ($icon)\n                <div class=\"{{ $iconWrapperClass }}\">\n                    @svg($icon, $iconClass)\n                </div>\n            @endif\n\n            <h3 class=\"{{ $titleClass }}\">{{ $title }}</h3>\n\n            @if ($description)\n                <p class=\"{{ $descriptionClass }}\">\n                    {{ $description }}\n                </p>\n            @endif\n\n            @if ($buttonHref && $buttonText)\n                <x-form.button class=\"{{ $buttonClass }}\" :href=\"$buttonHref\">\n                    {{ $buttonText }}\n                </x-form.button>\n            @endif\n        </div>\n    </x-card>\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/components/mdash.blade.php",
    "content": "<span class=\"text-gray-400\">-</span>\n"
  },
  {
    "path": "packages/frontend/resources/views/components/modal/body.blade.php",
    "content": "<div class=\"px-6 py-5 bg-base-900 text-base-200\">\n    {{ $slot }}\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/components/modal/footer.blade.php",
    "content": "<div class=\"px-6 py-4 bg-base-950 border-t border-base-800/50 flex items-center justify-end gap-3\">\n    {{ $slot }}\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/components/modal/header.blade.php",
    "content": "@props(['icon' => null, 'iconColor' => 'red', 'show' => 'show'])\n\n<div class=\"px-6 py-5 bg-gradient-to-r from-base-950 to-base-900 border-b border-base-800/50\">\n    <div class=\"flex items-center gap-4\">\n        @if($icon)\n            <div class=\"flex-shrink-0 flex items-center justify-center w-12 h-12 rounded-lg bg-{{ $iconColor }}/10 border border-{{ $iconColor }}/30\">\n                @svg($icon, 'w-6 h-6 text-' . $iconColor)\n            </div>\n        @endif\n        <div class=\"flex-1\">\n            <h3 class=\"text-xl font-bold text-base-50\">\n                {{ $slot }}\n            </h3>\n        </div>\n        <button \n            type=\"button\" \n            @click=\"{{ $show }} = false\"\n            class=\"flex-shrink-0 text-base-400 hover:text-base-200 transition-colors duration-200 p-1 rounded-lg hover:bg-base-800/50\"\n        >\n            <svg class=\"w-6 h-6\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\" />\n            </svg>\n        </button>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/components/modal.blade.php",
    "content": "@props(['show' => 'show', 'maxWidth' => '2xl'])\n\n@php\n$maxWidth = [\n    'sm' => 'sm:max-w-sm',\n    'md' => 'sm:max-w-md',\n    'lg' => 'sm:max-w-lg',\n    'xl' => 'sm:max-w-xl',\n    '2xl' => 'sm:max-w-2xl',\n][$maxWidth];\n@endphp\n\n<div\n    x-show=\"{{ $show }}\"\n    x-on:keydown.escape.window=\"{{ $show }} = false\"\n    class=\"fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-[99] flex items-center justify-center\"\n    style=\"display: none;\"\n    x-cloak\n>\n    <div\n        x-show=\"{{ $show }}\"\n        class=\"fixed inset-0 transform transition-all\"\n        x-on:click=\"{{ $show }} = false\"\n        x-transition:enter=\"ease-out duration-300\"\n        x-transition:enter-start=\"opacity-0\"\n        x-transition:enter-end=\"opacity-100\"\n        x-transition:leave=\"ease-in duration-200\"\n        x-transition:leave-start=\"opacity-100\"\n        x-transition:leave-end=\"opacity-0\"\n    >\n        <div class=\"absolute inset-0 bg-base-black/80 backdrop-blur-sm\"></div>\n    </div>\n\n    <div\n        x-show=\"{{ $show }}\"\n        class=\"relative bg-base-900 rounded-lg overflow-hidden shadow-xl transform transition-all w-full mx-auto {{ $maxWidth }}\"\n        x-transition:enter=\"ease-out duration-300\"\n        x-transition:enter-start=\"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\"\n        x-transition:enter-end=\"opacity-100 translate-y-0 sm:scale-100\"\n        x-transition:leave=\"ease-in duration-200\"\n        x-transition:leave-start=\"opacity-100 translate-y-0 sm:scale-100\"\n        x-transition:leave-end=\"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\"\n        @click.away=\"{{ $show }} = false\"\n    >\n        <div class=\"border border-base-700/50 rounded-lg overflow-hidden\">\n            {{ $slot }}\n        </div>\n    </div>\n</div>\n\n"
  },
  {
    "path": "packages/frontend/resources/views/components/page-header/actions/index.blade.php",
    "content": "<div class=\"hidden lg:block space-x-4\">\n    {{ $slot }}\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/components/page-header/mobile-actions/index.blade.php",
    "content": "<div class=\"lg:hidden\" x-data=\"{ open: false }\">\n    <button\n        x-ref=\"trigger\"\n        class=\"group relative flex items-center justify-center rounded-lg px-4 py-2.5 bg-base-900/50 border border-base-800/50 text-base-300 transition-all duration-200 hover:bg-base-800/50 hover:border-base-700 hover:text-base-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red\"\n        type=\"button\" x-on:click=\"open = !open\">\n        @svg('tni-menu-o', 'size-6 transition-colors duration-200')\n    </button>\n    \n    <template x-teleport=\"body\">\n        <div\n            x-show=\"open\" \n            @click.away=\"open = false\" \n            x-cloak\n            class=\"fixed z-[9999] w-56 overflow-hidden rounded-lg bg-base-950 border border-base-800/50 shadow-xl\"\n            x-transition:enter=\"transition ease-out duration-200\"\n            x-transition:enter-start=\"opacity-0 scale-95\"\n            x-transition:enter-end=\"opacity-100 scale-100\"\n            x-transition:leave=\"transition ease-in duration-150\"\n            x-transition:leave-start=\"opacity-100 scale-100\"\n            x-transition:leave-end=\"opacity-0 scale-95\"\n            :style=\"`top: ${$refs.trigger?.getBoundingClientRect().bottom + 8}px; right: ${window.innerWidth - $refs.trigger?.getBoundingClientRect().right}px;`\">\n            <div class=\"py-1\" role=\"menu\">\n                {{ $slot }}\n            </div>\n        </div>\n    </template>\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/components/stats-card.blade.php",
    "content": "@props(['title', 'icon' => null, 'trend' => null, 'trendUp' => null])\n\n<div class=\"group relative overflow-hidden rounded-xl border border-base-700 bg-base-850/50 backdrop-blur-sm p-6 transition-all duration-300 hover:border-base-600 hover:-translate-y-1 hover:shadow-xl hover:shadow-base-900/50\"\n     x-data=\"{\n         contentLength: 0,\n         fontSize: 'text-3xl md:text-4xl',\n         init() {\n             this.$nextTick(() => {\n                 const content = this.$refs.content.innerText.trim();\n                 this.contentLength = content.length;\n                 \n                 // Adjust font size based on content length\n                 if (this.contentLength > 50) {\n                     this.fontSize = 'text-sm md:text-base';\n                 } else if (this.contentLength > 30) {\n                     this.fontSize = 'text-base md:text-lg';\n                 } else if (this.contentLength > 20) {\n                     this.fontSize = 'text-lg md:text-xl';\n                 } else if (this.contentLength > 10) {\n                     this.fontSize = 'text-xl md:text-2xl';\n                 } else {\n                     this.fontSize = 'text-3xl md:text-4xl';\n                 }\n             });\n         }\n     }\">\n    <!-- Gradient background overlay with noise to prevent banding -->\n    <div class=\"absolute inset-0 -z-10\" \n         style=\"background: \n                linear-gradient(135deg, \n                    rgba(35, 35, 51, 1) 0%, \n                    rgba(33, 33, 48, 1) 10%,\n                    rgba(31, 31, 45, 1) 20%,\n                    rgba(29, 29, 42, 1) 30%,\n                    rgba(28, 28, 40, 1) 40%,\n                    rgba(27, 27, 38, 1) 50%,\n                    rgba(26, 26, 36, 1) 60%,\n                    rgba(26, 26, 36, 1) 70%,\n                    rgba(26, 26, 36, 1) 80%,\n                    rgba(26, 26, 36, 1) 90%,\n                    rgba(26, 26, 36, 1) 100%\n                ),\n                url('data:image/svg+xml,%3Csvg viewBox=%220 0 400 400%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noiseFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%221.5%22 numOctaves=%225%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noiseFilter)%22 opacity=%220.15%22/%3E%3C/svg%3E');\">\n    </div>\n    \n    <!-- Gradient accent line on top -->\n    <div class=\"absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-blue via-indigo to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300\"></div>\n    \n    <!-- Glow effect on hover -->\n    <div class=\"absolute -inset-0.5 bg-gradient-to-r from-blue/20 to-indigo/20 opacity-0 group-hover:opacity-100 blur-xl transition-opacity duration-500 -z-10\"></div>\n    \n    <div class=\"relative\">\n        <!-- Header with icon and trend -->\n        <div class=\"flex items-start justify-between mb-3\">\n            <div class=\"flex items-center gap-3\">\n                @if($icon)\n                    <div class=\"flex items-center justify-center w-10 h-10 rounded-lg bg-gradient-to-br from-blue/10 to-indigo/10 border border-blue/20 group-hover:border-blue/40 transition-colors duration-300\">\n                        @svg($icon, 'w-5 h-5 text-blue-light')\n                    </div>\n                @endif\n                <dt class=\"text-sm font-medium text-base-300 group-hover:text-base-200 transition-colors duration-300\">\n                    {{ $title }}\n                </dt>\n            </div>\n            \n            @if($trend !== null)\n                <div @class([\n                    'flex items-center gap-1 px-2 py-1 rounded-md text-xs font-semibold transition-all duration-300',\n                    'bg-green/10 text-green-light border border-green/30' => $trendUp,\n                    'bg-red/10 text-red-light border border-red/30' => !$trendUp,\n                ])>\n                    @if($trendUp)\n                        <svg class=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 10l7-7m0 0l7 7m-7-7v18\" />\n                        </svg>\n                    @else\n                        <svg class=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 14l-7 7m0 0l-7-7m7 7V3\" />\n                        </svg>\n                    @endif\n                    <span>{{ $trend }}%</span>\n                </div>\n            @endif\n        </div>\n        \n        <!-- Value with dynamic font size -->\n        <dd x-ref=\"content\" \n            :class=\"fontSize\"\n            class=\"font-bold tracking-tight bg-gradient-to-r from-base-50 to-base-100 bg-clip-text text-transparent group-hover:from-base-50 group-hover:via-base-50 group-hover:to-base-100 transition-all duration-300 break-words\">\n            {{ $slot }}\n        </dd>\n        \n        <!-- Decorative gradient line -->\n        <div class=\"mt-4 h-1 w-12 rounded-full bg-gradient-to-r from-blue via-indigo to-transparent opacity-60 group-hover:opacity-100 group-hover:w-20 transition-all duration-500\"></div>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/components/tabs/container.blade.php",
    "content": "@props([\n    'tabs' => [],\n    'activeTab' => '',\n])\n\n<div x-data=\"{ activeTab: '{{ $activeTab }}' }\" {{ $attributes }}>\n    {{ $slot }}\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/components/tabs/navigation.blade.php",
    "content": "@props([\n    'tabs' => [],\n])\n\n<div {{ $attributes->merge(['class' => 'mb-8']) }}>\n    <x-frontend::card :padding=\"false\" class=\"overflow-hidden\">\n        <div class=\"border-b border-base-700\">\n            <nav class=\"-mb-px flex space-x-1 p-2\" aria-label=\"Tabs\">\n                @foreach($tabs as $index => $tab)\n                    @if(!isset($tab['gate']) || auth()->user()->can($tab['gate']))\n                        <button\n                            @click=\"activeTab = '{{ $tab['key'] }}'\"\n                            :class=\"activeTab === '{{ $tab['key'] }}' ? 'border-{{ $tab['color'] ?? 'red' }} text-base-50 bg-base-800/50' : 'border-transparent text-base-300 hover:text-base-100 hover:border-base-600'\"\n                            class=\"group relative min-w-0 flex-1 sm:flex-initial overflow-hidden rounded-lg border-2 px-4 py-3 text-center text-sm font-medium transition-all duration-300 focus:z-10 focus:outline-none focus:ring-2 focus:ring-{{ $tab['color'] ?? 'red' }} focus:ring-offset-2 focus:ring-offset-base-900 @if($index === 0) border-{{ $tab['color'] ?? 'red' }} text-base-50 bg-base-800/50 @else border-transparent text-base-300 @endif\"\n                        >\n                            <span class=\"flex items-center justify-center gap-2\">\n                                @if(isset($tab['icon']))\n                                    @svg($tab['icon'], 'size-4')\n                                @endif\n                                <span class=\"hidden sm:inline\">{{ $tab['label'] }}</span>\n                            </span>\n                        </button>\n                    @endif\n                @endforeach\n            </nav>\n        </div>\n    </x-frontend::card>\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/components/tabs/panel.blade.php",
    "content": "@props([\n    'key' => '',\n    'cloak' => true,\n])\n\n<div x-show=\"activeTab === '{{ $key }}'\" \n     @if($cloak)x-cloak @endif\n     x-transition:enter=\"transition ease-out duration-300\"\n     x-transition:enter-start=\"opacity-0 transform translate-y-4\"\n     x-transition:enter-end=\"opacity-100 transform translate-y-0\"\n     {{ $attributes->merge(['class' => 'space-y-6']) }}>\n    {{ $slot }}\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/components/tabs/panels.blade.php",
    "content": "@props([\n    'tabs' => [],\n])\n\n<div {{ $attributes->merge(['class' => 'space-y-6']) }}>\n    @foreach($tabs as $index => $tab)\n        @if(!isset($tab['gate']) || auth()->user()->can($tab['gate']))\n            <div x-show=\"activeTab === '{{ $tab['key'] }}'\" \n                 @if($index !== 0)x-cloak @endif\n                 x-transition:enter=\"transition ease-out duration-300\"\n                 x-transition:enter-start=\"opacity-0 transform translate-y-4\"\n                 x-transition:enter-end=\"opacity-100 transform translate-y-0\"\n                 class=\"space-y-6\">\n                \n                @if(isset($tab['title']) || isset($tab['description']) || isset($tab['route']))\n                    <div class=\"flex items-center justify-between\">\n                        <div>\n                            @if(isset($tab['title']))\n                                <h2 class=\"text-2xl font-bold text-base-50 flex items-center gap-3\">\n                                    @if(isset($tab['icon']))\n                                        @svg($tab['icon'], 'size-6 text-' . ($tab['color'] ?? 'red'))\n                                    @endif\n                                    {{ $tab['title'] }}\n                                </h2>\n                            @endif\n                            @if(isset($tab['description']))\n                                <p class=\"text-base-300 mt-1\">{{ $tab['description'] }}</p>\n                            @endif\n                        </div>\n                        @if(isset($tab['route']))\n                            <a href=\"{{ $tab['route'] }}\" \n                               class=\"group flex items-center gap-2 px-4 py-2.5 rounded-lg border-2 border-base-700 bg-base-850/50 hover:border-{{ $tab['color'] ?? 'red' }} hover:bg-base-800/50 text-base-200 hover:text-base-50 transition-all duration-300 text-sm font-medium\">\n                                <span>@lang('View Details')</span>\n                                @svg('tni-right-o', 'size-4 group-hover:translate-x-1 transition-transform duration-300')\n                            </a>\n                        @endif\n                    </div>\n                @endif\n\n                <x-frontend::card>\n                    {{ $slot }}\n                </x-frontend::card>\n            </div>\n        @endif\n    @endforeach\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/integrations/table/actions-column.blade.php",
    "content": "<div class=\"flex items-center justify-end gap-x-3 w-full px-4\">\n    @foreach ($actions as $action)\n        @continue(!$action->isVisible($model))\n        <span class=\"min-w-0 text-sm font-semibold leading-6 text-white has-tooltip\"\n            x-on:click.stop.prevent=\"$wire.runInlineAction('{{ $action->code }}', {{ json_encode($model->getKey()) }})\">\n            <span class=\"tooltip tooltip-left rounded-sm shadow-lg p-2 bg-base-950 text-neutral-200 mt-8\">\n                {{ $action->name }}\n            </span>\n            @svg($action->icon, 'h-5 w-5 text-base-200 hover:text-red cursor-pointer')\n        </span>\n    @endforeach\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/integrations/table/chart-column.blade.php",
    "content": "<div class=\"max-w-32\">\n    @if (isset($title))\n        {!! $title !!}\n    @endif\n    <livewire:dynamic-component :is=\"$component\" :data=\"$parameters\" wire:key=\"{{ str()->random() . $component }}\" />\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/integrations/table/geoip-column.blade.php",
    "content": "@if(isset($country_code))\n    <div class=\"flex items-center gap-x-3 font-semibold text-white leading-6 text-sm\">\n    <span class=\"min-w-0 has-tooltip\">\n         <span class='tooltip rounded-sm shadow-lg p-2 bg-base-950 text-neutral-200 -mt-8 prose'>\n             {{ collect([$country_name, $region_name, $city, $isp, $org, $as])->whereNotNull()->implode(PHP_EOL) }}\n         </span>\n        <span class=\"truncate w-6\">\n            <x-icon name=\"flag-country-{{ $country_code }}\" class=\"h-4\"/>\n        </span>\n    </span>\n        <span>{{ $country_name }}</span>\n    </div\n@else\n    <span class=\"text-base-100 text-xs\">@lang('N/A')</span>\n@endif\n"
  },
  {
    "path": "packages/frontend/resources/views/integrations/table/hover-column.blade.php",
    "content": "<div class=\"flex items-center gap-x-3\">\n    <span class=\"min-w-0 text-sm font-semibold leading-6 text-white has-tooltip\">\n         <span class='tooltip rounded-sm shadow-lg p-2 bg-base-950 text-neutral-200 -mt-8'>\n             @if($raw)\n                 {!! $value !!}\n             @else\n                 {{ $value }}\n             @endif\n         </span>\n        <span class=\"truncate\">{{ $preview }}</span>\n    </span>\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/integrations/table/link-column.blade.php",
    "content": "<div class=\"px-3 py-2 truncate text-black dark:text-white\">\n    <a class=\"hover:underline\" href=\"{{ $link }}\" @if($newTab) target=\"_blank\" @endif>{{ $text }}</a>\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/integrations/table/status-column.blade.php",
    "content": "<div class=\"flex items-center gap-x-3\">\n    @if ($status !== null)\n        <div class=\"flex-none rounded-full p-1 text-gray-500 bg-gray-100/10\">\n            @if ($status === \\Vigilant\\Frontend\\Integrations\\Table\\Enums\\Status::Success)\n                <div class=\"h-2 w-2 rounded-full bg-green-light\"></div>\n            @elseif($status === \\Vigilant\\Frontend\\Integrations\\Table\\Enums\\Status::Warning)\n                <div class=\"h-2 w-2 rounded-full bg-orange-light animate-pulse\"></div>\n            @else\n                <div class=\"h-2 w-2 rounded-full bg-red-light animate-pulse\"></div>\n            @endif\n        </div>\n    @endif\n    @if ($text !== null)\n        <h2 class=\"min-w-0 text-sm font-semibold leading-6 text-white\">\n            <span class=\"truncate\">{{ $text }}</span>\n        </h2>\n    @else\n        <div class=\"px-3 py-2 truncate text-black dark:text-white\">\n            <span class=\"opacity-25\">&mdash;</span>\n        </div>\n    @endif\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/livewire/charts/base-chart-placeholder.blade.php",
    "content": "<div @class([\n    'bg-base-700 animate-pulse rounded-sm flex items-center justify-center',\n    'bg-base-950 py-4 px-2 rounded-md border border-base-800' => $addStyle,\n])>\n    <div style=\"height: {{ $height }}px;\" class=\"flex items-center justify-center\">\n        <span class=\"text-base-400 text-sm\">Loading Chart</span>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/frontend/resources/views/livewire/charts/base-chart.blade.php",
    "content": "<div wire:init=\"loadChart\" x-data=\"{ show: false, loading: true }\" @class([\n    'bg-base-950 py-4 px-2 rounded-md border border-base-800' => $addStyle,\n])>\n    <div style=\"height: {{ $height }}px;\" wire:ignore x-init=\"() => {\n        Livewire.on('{{ $identifier }}-update-chart', params => {\n            config = params[0]\n\n            show = config.data.labels.length > 0\n            loading = false\n\n            let chart = Chart.getChart('{{ $identifier }}');\n\n            config.options.plugins.tooltip.callbacks = {\n                label: function(context) {\n                    let unit = context.dataset.unit || '';\n\n                    return context.dataset.label + ': ' + context.formattedValue + ' ' + unit;\n                }\n            };\n\n            if (typeof chart === 'undefined') {\n                chart = new Chart(document.getElementById('{{ $identifier }}'), config);\n            } else {\n                chart.reset();\n                chart.type = config.type;\n                chart.data = config.data;\n                chart.options = config.options;\n                chart.update();\n            }\n        });\n    }\">\n        <canvas wire:ignore id=\"{{ $identifier }}\"></canvas>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/frontend/src/Concerns/DisplaysAlerts.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Concerns;\n\nuse Vigilant\\Frontend\\Enums\\AlertType;\n\n// @phpstan-ignore-next-line\ntrait DisplaysAlerts\n{\n    protected function alert(string $title, string $message = '', AlertType $type = AlertType::Info): void\n    {\n        session()->flash('alert');\n        session()->flash('alert-title', $title);\n        session()->flash('alert-message', $message);\n        session()->flash('alert-type', $type);\n    }\n\n    protected function alertBrowser(string $title, string $message = '', AlertType $type = AlertType::Info): void\n    {\n        $this->dispatch('alert', [\n            'id' => uniqid(),\n            'title' => $title,\n            'message' => $message,\n            'type' => $type->value,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Enums/AlertType.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Enums;\n\nenum AlertType: string\n{\n    case Info = 'info';\n    case Success = 'success';\n    case Warning = 'warning';\n    case Danger = 'danger';\n\n    public function component(): string\n    {\n        return 'alerts.'.$this->value;\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Http/Livewire/BaseChart.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Http\\Livewire;\n\nuse Illuminate\\Support\\Str;\nuse Illuminate\\View\\View;\nuse Livewire\\Component;\n\nabstract class BaseChart extends Component\n{\n    public int $height = 200;\n\n    public bool $addStyle = true;\n\n    abstract public function data(): array;\n\n    public function loadChart(): void\n    {\n        $data = array_replace_recursive($this->defaultOptions(), $this->data());\n\n        $this->dispatch($this->getIdentifier().'-update-chart', $data);\n    }\n\n    public function placeholder(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'frontend::livewire.charts.base-chart-placeholder';\n\n        return view($view, [\n            'height' => $this->height,\n        ]);\n    }\n\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'frontend::livewire.charts.base-chart';\n\n        return view($view, [\n            'identifier' => $this->getIdentifier(),\n            'height' => $this->height,\n        ]);\n    }\n\n    public function defaultOptions(): array\n    {\n        return [\n            'options' => [\n                'maintainAspectRatio' => false,\n                'responsive' => true,\n                'plugins' => [\n                    'legend' => [\n                        'position' => 'top',\n                        'align' => 'end',\n                        'labels' => [\n                            'color' => $this->getColor('text-secondary'),\n                            'font' => [\n                                'size' => 12,\n                            ],\n                            'padding' => 12,\n                            'usePointStyle' => true,\n                            'pointStyle' => 'circle',\n                        ],\n                    ],\n                    'title' => [\n                        'display' => false,\n                    ],\n                    'tooltip' => [\n                        'position' => 'average',\n                        'mode' => 'index',\n                        'intersect' => false,\n                        'backgroundColor' => $this->getColor('bg-elevated'),\n                        'titleColor' => $this->getColor('text-primary'),\n                        'bodyColor' => $this->getColor('text-secondary'),\n                        'borderColor' => $this->getColor('border'),\n                        'borderWidth' => 1,\n                        'padding' => 12,\n                    ],\n                ],\n                'scales' => [\n                    'x' => [\n                        'title' => [\n                            'color' => $this->getColor('text-muted'),\n                        ],\n                        'grid' => [\n                            'display' => false,\n                        ],\n                        'ticks' => [\n                            'color' => $this->getColor('text-muted'),\n                            'font' => [\n                                'size' => 11,\n                            ],\n                            'maxRotation' => 0,\n                        ],\n                    ],\n                    'y' => [\n                        'title' => [\n                            'color' => $this->getColor('text-muted'),\n                        ],\n                        'grid' => [\n                            'color' => $this->getColor('grid'),\n                            'drawBorder' => false,\n                        ],\n                        'ticks' => [\n                            'color' => $this->getColor('text-muted'),\n                            'font' => [\n                                'size' => 11,\n                            ],\n                        ],\n                    ],\n                ],\n                'interaction' => [\n                    'mode' => 'index',\n                    'intersect' => false,\n                ],\n            ],\n        ];\n    }\n\n    protected function dataset(array $dataset): array\n    {\n        return array_merge([\n            'pointRadius' => 1,\n            'pointHoverRadius' => 4,\n            'borderCapStyle' => 'round',\n            'borderJoinStyle' => 'round',\n            'borderWidth' => 2,\n            'tension' => 0.4,\n        ], $dataset);\n    }\n\n    /**\n     * Get a color from the design system\n     *\n     * @param  string  $key  Color key\n     * @return string Hex color or rgba string\n     */\n    protected function getColor(string $key): string\n    {\n        return match ($key) {\n            // Text colors\n            'text-primary' => '#F4F4FA',     // base-100\n            'text-secondary' => '#D8D8E8',   // base-200\n            'text-muted' => '#A8A8C0',       // base-400\n\n            // Background colors\n            'bg-elevated' => '#232333',      // base-850\n            'bg-main' => '#1A1A24',          // base-900\n\n            // Border colors\n            'border' => '#444459',           // base-700\n            'grid' => '#2D2D42',             // base-800\n\n            default => '#F4F4FA',\n        };\n    }\n\n    /**\n     * Get chart line colors from the design system\n     * Returns an array of color sets with border and background\n     *\n     * @return array<int, array{border: string, bg: string}>\n     */\n    protected function getChartColors(): array\n    {\n        return [\n            ['border' => '#3B82F6', 'bg' => 'rgba(59, 130, 246, 0.1)'],   // blue\n            ['border' => '#6366F1', 'bg' => 'rgba(99, 102, 241, 0.1)'],   // indigo\n            ['border' => '#10B981', 'bg' => 'rgba(16, 185, 129, 0.1)'],   // green\n            ['border' => '#F97316', 'bg' => 'rgba(249, 115, 22, 0.1)'],   // orange\n            ['border' => '#8B5CF6', 'bg' => 'rgba(139, 92, 246, 0.1)'],   // purple\n            ['border' => '#EC4899', 'bg' => 'rgba(236, 72, 153, 0.1)'],   // magenta\n            ['border' => '#06B6D4', 'bg' => 'rgba(6, 182, 212, 0.1)'],    // cyan\n            ['border' => '#EF4444', 'bg' => 'rgba(239, 68, 68, 0.1)'],    // red\n        ];\n    }\n\n    /**\n     * Get a specific chart color by index\n     *\n     * @param  int  $index  Color index (cycles through available colors)\n     * @return array{border: string, bg: string}\n     */\n    protected function getChartColor(int $index): array\n    {\n        $colors = $this->getChartColors();\n\n        return $colors[$index % count($colors)];\n    }\n\n    protected function getIdentifier(): string\n    {\n        return Str::slug(get_class($this));\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Integrations/Table/Actions/InlineAction.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Integrations\\Table\\Actions;\n\nuse Closure;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse RamonRietdijk\\LivewireTables\\Concerns\\Makeable;\n\nclass InlineAction\n{\n    use Makeable;\n\n    public ?Closure $visible = null;\n\n    public function __construct(\n        public string $code,\n        public string $name,\n        public string $icon\n    ) {}\n\n    public function visible(Closure $callback): static\n    {\n        $this->visible = $callback;\n\n        return $this;\n    }\n\n    public function isVisible(Model $model): bool\n    {\n        if ($this->visible === null) {\n            return true;\n        }\n\n        return ($this->visible)($model);\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Integrations/Table/ActionsColumn.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Integrations\\Table;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse RamonRietdijk\\LivewireTables\\Columns\\BaseColumn;\n\nclass ActionsColumn extends BaseColumn\n{\n    public array $actions = [];\n\n    public function actions(array $actions): static\n    {\n        $this->actions = $actions;\n\n        return $this;\n    }\n\n    public function render(Model $model): mixed\n    {\n        return view('frontend::integrations.table.actions-column', [\n            'actions' => $this->actions,\n            'model' => $model,\n            'column' => $this,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Integrations/Table/BaseTable.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Vigilant\\Frontend\\Integrations\\Table;\n\nuse RamonRietdijk\\LivewireTables\\Livewire\\LivewireTable;\n\nabstract class BaseTable extends LivewireTable\n{\n    protected bool $useNavigate = true;\n}\n"
  },
  {
    "path": "packages/frontend/src/Integrations/Table/ChartColumn.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Integrations\\Table;\n\nuse Closure;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse RamonRietdijk\\LivewireTables\\Columns\\BaseColumn;\n\nclass ChartColumn extends BaseColumn\n{\n    public string $component = '';\n\n    public Closure $parameterCallback;\n\n    public function component(string $component): static\n    {\n        $this->component = $component;\n\n        return $this;\n    }\n\n    public function parameters(Closure $parameterCallback): static\n    {\n        $this->parameterCallback = $parameterCallback;\n\n        return $this;\n    }\n\n    public function render(Model $model): mixed\n    {\n        return view('frontend::integrations.table.chart-column', [\n            'component' => $this->component,\n            'parameters' => ($this->parameterCallback)($model),\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Integrations/Table/Concerns/HasInlineActions.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Integrations\\Table\\Concerns;\n\ntrait HasInlineActions\n{\n    public function runInlineAction(string $code, mixed $id): void\n    {\n        $this->runAction($code, [$id]);\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Integrations/Table/DateColumn.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Integrations\\Table;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Carbon;\nuse RamonRietdijk\\LivewireTables\\Columns\\DateColumn as BaseDateColumn;\n\n/* Extended to handle the team's timezone */\nclass DateColumn extends BaseDateColumn\n{\n    public function resolveValue(Model $model): mixed\n    {\n        /** @var string|Carbon|null $value */\n        $value = $this->getValue($model);\n\n        if ($this->displayUsing !== null) {\n            return call_user_func($this->displayUsing, $value, $model);\n        }\n\n        if ($value === null) {\n            return null;\n        }\n\n        /** @var Carbon $date */\n        $date = teamTimezone(Carbon::parse($value));\n\n        return $this->format === null\n            ? $date->toDateTimeString()\n            : $date->format($this->format);\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Integrations/Table/Enums/Status.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Integrations\\Table\\Enums;\n\nenum Status: string\n{\n    case Success = 'success';\n    case Warning = 'warning';\n    case Danger = 'danger';\n}\n"
  },
  {
    "path": "packages/frontend/src/Integrations/Table/GeoIpColumn.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Integrations\\Table;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\n\nclass GeoIpColumn extends Column\n{\n    public function render(Model $model): mixed\n    {\n        $geoip = $model['geoip'] ?? [];\n\n        return view('frontend::integrations.table.geoip-column', $geoip);\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Integrations/Table/HoverColumn.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Integrations\\Table;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Str;\nuse RamonRietdijk\\LivewireTables\\Columns\\BaseColumn;\n\nclass HoverColumn extends BaseColumn\n{\n    protected int $maxLength = 60;\n\n    public function length(int $length): static\n    {\n        $this->maxLength = $length;\n\n        return $this;\n    }\n\n    public function render(Model $model): mixed\n    {\n        $value = $this->resolveValue($model);\n\n        return view('frontend::integrations.table.hover-column', [\n            'value' => $value,\n            'preview' => strip_tags(Str::limit($value, $this->maxLength)),\n            'raw' => $this->raw,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Integrations/Table/LinkColumn.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Integrations\\Table;\n\nuse Closure;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse RamonRietdijk\\LivewireTables\\Columns\\BaseColumn;\n\nclass LinkColumn extends BaseColumn\n{\n    public ?Closure $linkCallback = null;\n\n    public ?Closure $textCallback = null;\n\n    public bool $newTab = false;\n\n    public function link(Closure $linkCallback): static\n    {\n        $this->linkCallback = $linkCallback;\n\n        return $this;\n    }\n\n    public function text(Closure $textCallback): static\n    {\n        $this->textCallback = $textCallback;\n\n        return $this;\n    }\n\n    public function openInNewTab(bool $newTab = true): static\n    {\n        $this->newTab = $newTab;\n\n        return $this;\n    }\n\n    public function render(Model $model): mixed\n    {\n        $url = $this->linkCallback !== null ? ($this->linkCallback)($model) : $this->resolveValue($model);\n        $text = $this->textCallback !== null ? ($this->textCallback)($model) : $url;\n\n        return view('frontend::integrations.table.link-column', [\n            'link' => $url,\n            'text' => $text,\n            'newTab' => $this->newTab,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Integrations/Table/StatusColumn.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Integrations\\Table;\n\nuse Closure;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse RamonRietdijk\\LivewireTables\\Columns\\BaseColumn;\n\nclass StatusColumn extends BaseColumn\n{\n    public Closure $statusCallback;\n\n    public Closure $textCallback;\n\n    public function status(Closure $statusCallback): static\n    {\n        $this->statusCallback = $statusCallback;\n\n        return $this;\n    }\n\n    public function text(Closure $textCallback): static\n    {\n        $this->textCallback = $textCallback;\n\n        return $this;\n    }\n\n    public function render(Model $model): mixed\n    {\n        return view('frontend::integrations.table.status-column', [\n            'text' => ($this->textCallback)($model),\n            'status' => ($this->statusCallback)($model),\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend;\n\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function boot(): void\n    {\n        $this\n            ->bootViews()\n            ->bootLivewire();\n    }\n\n    protected function bootViews(): static\n    {\n        $this->loadViewsFrom(__DIR__.'/../resources/views', 'frontend');\n\n        return $this;\n    }\n\n    protected function bootLivewire(): static\n    {\n        // Livewire::component('sites', Sites::class);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Traits/CanBeInline.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Traits;\n\n// @phpstan-ignore-next-line\ntrait CanBeInline\n{\n    public bool $inline = false;\n}\n"
  },
  {
    "path": "packages/frontend/src/Validation/CleanDomainValidator.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Validation;\n\nuse Closure;\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\n\nclass CleanDomainValidator implements ValidationRule\n{\n    public function validate(string $attribute, mixed $value, Closure $fail): void\n    {\n        $value = (string) $value;\n\n        if ($value === '') {\n            return;\n        }\n        if ($this->containsUrlSpecificCharacters($value)) {\n            $fail(__('Please enter only the domain (e.g., govigilant.io)'));\n        }\n    }\n\n    private function containsUrlSpecificCharacters(string $value): bool\n    {\n        return strpbrk($value, '/:#?') !== false;\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Validation/CountryCode.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Validation;\n\nuse Closure;\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\nuse League\\ISO3166\\ISO3166;\n\nclass CountryCode implements ValidationRule\n{\n    public function __construct(\n        protected string $format = 'alpha2',\n        protected ?ISO3166 $iso3166 = null,\n    ) {\n    }\n\n    public function validate(string $attribute, mixed $value, Closure $fail): void\n    {\n        if (! is_string($value) || $value === '') {\n            $fail(__('The :attribute field must be a valid country code.'));\n\n            return;\n        }\n\n        $value = strtoupper(trim($value));\n\n        /** @var ISO3166 $iso3166 */\n        $iso3166 = $this->iso3166 ??= new ISO3166;\n\n        try {\n            match ($this->format) {\n                'alpha3' => $iso3166->alpha3($value),\n                'numeric' => $iso3166->numeric($value),\n                default => $iso3166->alpha2($value),\n            };\n        } catch (\\Throwable) {\n            $fail(__('The :attribute field must be a valid country code.'));\n        }\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Validation/CronExpression.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Validation;\n\nuse Closure;\nuse Cron\\CronExpression as Cron;\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\n\nclass CronExpression implements ValidationRule\n{\n    public function validate(string $attribute, $value, Closure $fail): void\n    {\n        if (Cron::isValidExpression($value) === false) {\n            $fail(\"The $attribute field is not a valid cron expression.\");\n        }\n    }\n}\n"
  },
  {
    "path": "packages/frontend/src/Validation/Fqdn.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Validation;\n\nuse Closure;\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\n\nclass Fqdn implements ValidationRule\n{\n    public function __construct(protected bool $allowSubdomains = true) {}\n\n    public function validate(string $attribute, mixed $value, Closure $fail): void\n    {\n        if ($this->allowSubdomains) {\n            $pattern = '/^(?:[a-z0-9-]+\\.)*[a-z0-9-]+\\.[a-z]{2,}$/i';\n        } else {\n            $pattern = '/^[a-z0-9-]+\\.[a-z]{2,}$/i';\n        }\n\n        if (! preg_match($pattern, $value)) {\n            $fail(__('Invalid domain name, please enter a domain name + tld. For example: govigilant.io'));\n        }\n    }\n}\n"
  },
  {
    "path": "packages/frontend/testbench.yaml",
    "content": "providers:\n  - Vigilant\\Frontend\\ServiceProvider\n"
  },
  {
    "path": "packages/frontend/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\Frontend\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Frontend\\ServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/healthchecks/composer.json",
    "content": "{\n    \"name\": \"vigilant/healthchecks\",\n    \"description\": \"Vigilant Healthchecks\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"guzzlehttp/guzzle\": \"^7.8\",\n        \"laravel/framework\": \"^12.0\",\n        \"livewire/livewire\": \"^3.4\",\n        \"vigilant/core\": \"@dev\",\n        \"vigilant/frontend\": \"@dev\",\n        \"vigilant/notifications\": \"@dev\",\n        \"vigilant/sites\": \"@dev\",\n        \"vigilant/users\": \"@dev\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Healthchecks\\\\\": \"src\",\n            \"Vigilant\\\\Healthchecks\\\\Database\\\\Factories\\\\\": \"database/factories\",\n            \"Vigilant\\\\Users\\\\Database\\\\Factories\\\\\": \"../users/database/factories\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Healthchecks\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\Healthchecks\\\\ServiceProvider\"\n            ]\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/healthchecks/config/healthchecks.php",
    "content": "<?php\n\nreturn [\n    'queue' => 'healthchecks',\n\n    'http_timeout' => env('HEALTHCHECKS_HTTP_TIMEOUT', 10),\n\n    'http_max_attempts' => env('HEALTHCHECKS_HTTP_MAX_ATTEMPTS', 2),\n\n    'intervals' => [\n        60 => 'Every minute',\n        300 => 'Every 5 minutes',\n        600 => 'Every 10 minutes',\n        1800 => 'Every 30 minutes',\n        3600 => 'Every hour',\n        21600 => 'Every 6 hours',\n        43200 => 'Every 12 hours',\n        86400 => 'Daily',\n    ],\n];\n"
  },
  {
    "path": "packages/healthchecks/database/migrations/2025_11_06_200000_create_healthchecks_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('healthchecks', function (Blueprint $table): void {\n            $table->id();\n            $table->foreignIdFor(Site::class)->nullable()->constrained()->onDelete('cascade');\n            $table->foreignIdFor(Team::class)->constrained()->onDelete('cascade');\n            $table->boolean('enabled')->default(true);\n\n            $table->string('domain');\n            $table->string('type');\n            $table->string('endpoint')->nullable();\n            $table->string('token');\n\n            $table->dateTime('next_check_at')->nullable();\n            $table->dateTime('last_check_at')->nullable();\n            $table->integer('interval');\n\n            $table->string('status')->nullable();\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('healthchecks');\n    }\n};\n"
  },
  {
    "path": "packages/healthchecks/database/migrations/2025_11_06_201000_create_healthcheck_results_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('healthcheck_results', function (Blueprint $table): void {\n            $table->id();\n            $table->foreignIdFor(Healthcheck::class)->constrained()->onDelete('cascade');\n            $table->integer('run_id')->nullable();\n\n            $table->string('key');\n            $table->string('status');\n            $table->string('message')->nullable();\n            $table->json('data')->nullable();\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('healthcheck_results');\n    }\n};\n"
  },
  {
    "path": "packages/healthchecks/database/migrations/2025_11_06_202000_create_healthcheck_metrics_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('healthcheck_metrics', function (Blueprint $table): void {\n            $table->id();\n            $table->foreignIdFor(Healthcheck::class)->constrained()->onDelete('cascade');\n            $table->integer('run_id')->nullable();\n\n            $table->string('key');\n            $table->decimal('value');\n            $table->string('unit')->nullable();\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('healthcheck_metrics');\n    }\n};\n"
  },
  {
    "path": "packages/healthchecks/database/migrations/2025_11_23_150400_update_healthcheck_results_columns.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('healthcheck_results', function (Blueprint $table): void {\n            $table->dropColumn('run_id');\n            $table->timestamp('last_checked_at')->nullable()->after('data');\n            $table->timestamp('last_unhealthy_at')->nullable()->after('last_checked_at');\n            $table->unique(['healthcheck_id', 'key'], 'unique_healthcheck_result');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('healthcheck_results', function (Blueprint $table): void {\n            $table->dropUnique('unique_healthcheck_result');\n            $table->dropColumn('last_unhealthy_at');\n            $table->dropColumn('last_checked_at');\n            $table->integer('run_id')->nullable()->after('healthcheck_id');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/healthchecks/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n    ignoreErrors:\n        - identifier: missingType.iterableValue\n        - identifier: missingType.generics\n"
  },
  {
    "path": "packages/healthchecks/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/healthchecks/resources/navigation.php",
    "content": "<?php\n\nuse Vigilant\\Core\\Facades\\Navigation;\n\nNavigation::add(route('healthchecks.index'), 'Healthchecks')\n    ->icon('phosphor-heartbeat')\n    ->parent('health')\n    ->gate('use-healthchecks')\n    ->routeIs('healthchecks*')\n    ->sort(2);\n"
  },
  {
    "path": "packages/healthchecks/resources/views/components/empty-states/healthchecks.blade.php",
    "content": "<x-frontend::empty-state :title=\"__('No Healthchecks Yet')\" :description=\"__('Healthchecks help you monitor the health of your application.')\" icon=\"phosphor-warning-circle\"\n    iconClass=\"h-12 w-12 text-blue\" iconWrapperClass=\"rounded-full bg-blue/10 p-4 mb-6\" :buttonHref=\"route('healthchecks.create')\"\n    :buttonText=\"__('Add Healthcheck')\"\n    buttonClass=\"bg-gradient-to-r from-blue via-indigo to-purple bg-300% hover:shadow-lg hover:shadow-blue/30 transition-all duration-300\" />\n"
  },
  {
    "path": "packages/healthchecks/resources/views/healthcheck/view.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header :back=\"route('healthchecks.index')\" :title=\"'Healthcheck - ' . $healthcheck->domain . (!$healthcheck->enabled ? ' (Disabled)' : '')\">\n            <x-frontend::page-header.actions>\n                @if ($healthcheck->type !== \\Vigilant\\Healthchecks\\Enums\\Type::Endpoint)\n                    <x-form.button :href=\"route('healthchecks.setup', ['healthcheck' => $healthcheck])\">\n                        @lang('Setup')\n                    </x-form.button>\n                @endif\n                <x-form.button dusk=\"healthcheck-edit-button\" :href=\"route('healthchecks.edit', ['healthcheck' => $healthcheck])\">\n                    @lang('Edit')\n                </x-form.button>\n                <x-form.button class=\"bg-red\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.button>\n            </x-frontend::page-header.actions>\n\n            <x-frontend::page-header.mobile-actions>\n                @if ($healthcheck->type !== \\Vigilant\\Healthchecks\\Enums\\Type::Endpoint)\n                    <x-form.dropdown-button :href=\"route('healthchecks.setup', ['healthcheck' => $healthcheck])\">\n                        @lang('Setup')\n                    </x-form.dropdown-button>\n                @endif\n                <x-form.dropdown-button :href=\"route('healthchecks.edit', ['healthcheck' => $healthcheck])\">\n                    @lang('Edit')\n                </x-form.dropdown-button>\n                <x-form.dropdown-button class=\"!text-red hover:!text-red-light\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.dropdown-button>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    <div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4\">\n        <x-frontend::stats-card :title=\"__('Domain')\">\n            {{ $healthcheck->domain }}\n        </x-frontend::stats-card>\n\n        <x-frontend::stats-card :title=\"__('Last Check')\">\n            {{ $healthcheck->last_check_at ? $healthcheck->last_check_at->diffForHumans() : __('Never') }}\n        </x-frontend::stats-card>\n\n        <x-frontend::stats-card :title=\"__('Status')\">\n            @if ($healthcheck->status === \\Vigilant\\Healthchecks\\Enums\\Status::Healthy)\n                <span class=\"text-green-light\">{{ __('Healthy') }}</span>\n            @elseif($healthcheck->status === \\Vigilant\\Healthchecks\\Enums\\Status::Warning)\n                <span class=\"text-orange\">{{ __('Warning') }}</span>\n            @elseif($healthcheck->status === \\Vigilant\\Healthchecks\\Enums\\Status::Unhealthy)\n                <span class=\"text-red\">{{ __('Unhealthy') }}</span>\n            @else\n                <span class=\"text-neutral-400\">{{ __('Unknown') }}</span>\n            @endif\n        </x-frontend::stats-card>\n\n        <x-frontend::stats-card :title=\"__('Interval')\">\n            {{ $healthcheck->interval }}s\n        </x-frontend::stats-card>\n    </div>\n\n    <div class=\"mt-4\">\n        <h2 class=\"text-xl font-bold leading-7 sm:truncate sm:text-2xl sm:tracking-tight text-neutral-100 mb-2\">\n            {{ __('Metrics') }}\n        </h2>\n\n        <livewire:healthcheck-metric-chart :data=\"['healthcheckId' => $healthcheck->id]\" wire:key=\"metric-chart\" />\n    </div>\n\n    <div class=\"mt-4\">\n        <h2 class=\"text-xl font-bold leading-7 sm:truncate sm:text-2xl sm:tracking-tight text-neutral-100 mb-2\">\n            {{ __('Results') }}\n        </h2>\n\n        <livewire:healthcheck-result-table :healthcheckId=\"$healthcheck->id\" wire:key=\"result-table\" />\n    </div>\n\n    <!-- Delete Confirmation Modal -->\n    <div x-data=\"{ showDeleteModal: false }\" @open-delete-modal.window=\"showDeleteModal = true\">\n        <x-frontend::modal show=\"showDeleteModal\">\n            <x-frontend::modal.header icon=\"phosphor-trash\" iconColor=\"red\" show=\"showDeleteModal\">\n                @lang('Delete Healthcheck')\n            </x-frontend::modal.header>\n\n            <x-frontend::modal.body>\n                <div class=\"space-y-4\">\n                    <p class=\"text-base-100\">\n                        @lang('Are you sure you want to delete this healthcheck?')\n                    </p>\n                    <div class=\"bg-base-850 border border-base-700 rounded-lg p-4\">\n                        <div class=\"flex items-start gap-3\">\n                            <div class=\"flex-shrink-0\">\n                                @svg('phosphor-warning-circle', 'w-5 h-5 text-orange mt-0.5')\n                            </div>\n                            <div class=\"flex-1\">\n                                <p class=\"text-sm text-base-300\">\n                                    <span class=\"font-semibold text-base-100\">{{ $healthcheck->domain }}</span>\n                                </p>\n                                <p class=\"text-sm text-base-400 mt-1\">\n                                    @lang('This action cannot be undone. All healthcheck history and results for this monitor will be permanently deleted.')\n                                </p>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </x-frontend::modal.body>\n\n            <x-frontend::modal.footer>\n                <x-form.button type=\"button\" @click=\"showDeleteModal = false\">\n                    @lang('Cancel')\n                </x-form.button>\n                <form action=\"{{ route('healthchecks.delete', ['healthcheck' => $healthcheck]) }}\" method=\"POST\"\n                    class=\"inline\">\n                    @csrf\n                    @method('DELETE')\n                    <x-form.button class=\"bg-red\" type=\"submit\">\n                        @lang('Delete Healthcheck')\n                    </x-form.button>\n                </form>\n            </x-frontend::modal.footer>\n        </x-frontend::modal>\n    </div>\n\n</x-app-layout>\n"
  },
  {
    "path": "packages/healthchecks/resources/views/livewire/charts/metric-chart.blade.php",
    "content": "<div class=\"bg-base-950 py-4 px-2 rounded-md border border-base-800\">\n    <div class=\"flex justify-between items-start mb-3\">\n        @if (count($availableKeys))\n            <div class=\"ml-3 space-y-2 flex-1\">\n                <div class=\"flex flex-wrap gap-2 items-center\">\n                    @foreach ($availableKeys as $key)\n                        <button wire:click=\"setMetricKey('{{ $key }}')\" wire:loading.attr=\"disabled\"\n                            wire:loading.class=\"opacity-50 cursor-not-allowed\" @class([\n                                'px-3 py-1 text-xs font-medium rounded-full transition-colors duration-200 cursor-pointer relative',\n                                'bg-blue text-white' => $selectedKey === $key,\n                                'bg-base-800 text-base-200 hover:bg-base-700' => $selectedKey !== $key,\n                            ])>\n                            {{ $key }}\n                        </button>\n                    @endforeach\n                </div>\n            </div>\n        @else\n            <div class=\"flex-1 ml-3\">\n                <p class=\"text-sm text-base-400\">{{ __('No metrics available') }}</p>\n            </div>\n        @endif\n\n        <div class=\"mr-2 relative\" x-data=\"{ open: false }\">\n            <button @click=\"open = !open\" @click.away=\"open = false\"\n                class=\"px-3 py-1.5 text-xs font-medium rounded-md bg-base-800 text-base-200 hover:bg-base-700 transition-colors duration-200 flex items-center gap-2\"\n                wire:loading.attr=\"disabled\" wire:loading.class=\"opacity-50 cursor-not-allowed\">\n                <span>{{ $dateRangeOptions[$dateRange] }}</span>\n                <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n                </svg>\n            </button>\n\n            <div x-show=\"open\" x-transition:enter=\"transition ease-out duration-100\"\n                x-transition:enter-start=\"transform opacity-0 scale-95\"\n                x-transition:enter-end=\"transform opacity-100 scale-100\"\n                x-transition:leave=\"transition ease-in duration-75\"\n                x-transition:leave-start=\"transform opacity-100 scale-100\"\n                x-transition:leave-end=\"transform opacity-0 scale-95\"\n                class=\"absolute right-0 mt-2 w-40 rounded-md shadow-lg bg-base-900 border border-base-800 z-10\"\n                style=\"display: none;\">\n                <div class=\"py-1\">\n                    @foreach ($dateRangeOptions as $key => $label)\n                        <button wire:click=\"setDateRange('{{ $key }}')\" @click=\"open = false\"\n                            @class([\n                                'block w-full text-left px-4 py-2 text-sm transition-colors duration-200',\n                                'bg-blue text-white' => $dateRange === $key,\n                                'text-base-200 hover:bg-base-800' => $dateRange !== $key,\n                            ])>\n                            {{ $label }}\n                        </button>\n                    @endforeach\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div wire:init=\"loadChart\" x-data=\"{ show: false, loading: true }\">\n        <div style=\"height: {{ $height }}px;\" wire:ignore x-init=\"() => {\n            Livewire.on('{{ $identifier }}-update-chart', params => {\n                config = params[0]\n\n                show = config.data.labels.length > 0\n                loading = false\n\n                let chart = Chart.getChart('{{ $identifier }}');\n\n                config.options.plugins.tooltip.callbacks = {\n                    label: function(context) {\n                        let unit = context.dataset.unit || '';\n\n                        return context.dataset.label + ': ' + context.formattedValue + ' ' + unit;\n                    }\n                };\n\n                if (typeof chart === 'undefined') {\n                    chart = new Chart(document.getElementById('{{ $identifier }}'), config);\n                } else {\n                    chart.reset();\n                    chart.type = config.type;\n                    chart.data = config.data;\n                    chart.options = config.options;\n                    chart.update();\n                }\n            });\n        }\">\n            <canvas wire:ignore id=\"{{ $identifier }}\"></canvas>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/healthchecks/resources/views/livewire/healthcheck-dashboard.blade.php",
    "content": "<div>\n    <div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6\">\n        <x-frontend::stats-card :title=\"__('Domain')\">\n            {{ $healthcheck->domain }}\n        </x-frontend::stats-card>\n\n        <x-frontend::stats-card :title=\"__('Last Check')\">\n            {{ $healthcheck->last_check_at ? $healthcheck->last_check_at->diffForHumans() : __('Never') }}\n        </x-frontend::stats-card>\n\n        <x-frontend::stats-card :title=\"__('Status')\">\n            @if($healthcheck->status === \\Vigilant\\Healthchecks\\Enums\\Status::Healthy)\n                <span class=\"text-green-light\">{{ __('Healthy') }}</span>\n            @elseif($healthcheck->status === \\Vigilant\\Healthchecks\\Enums\\Status::Warning)\n                <span class=\"text-orange\">{{ __('Warning') }}</span>\n            @elseif($healthcheck->status === \\Vigilant\\Healthchecks\\Enums\\Status::Unhealthy)\n                <span class=\"text-red\">{{ __('Unhealthy') }}</span>\n            @else\n                <span class=\"text-neutral-400\">{{ __('Unknown') }}</span>\n            @endif\n        </x-frontend::stats-card>\n\n        <x-frontend::stats-card :title=\"__('Interval')\">\n            {{ $healthcheck->interval }}s\n        </x-frontend::stats-card>\n    </div>\n\n    <div class=\"space-y-6\">\n        <div>\n            <h3 class=\"text-lg font-semibold text-base-50 mb-3\">{{ __('Metrics') }}</h3>\n            <livewire:healthcheck-metric-chart :data=\"['healthcheckId' => $healthcheck->id]\" wire:key=\"metric-chart-{{ $healthcheck->id }}\" />\n        </div>\n\n        <div>\n            <h3 class=\"text-lg font-semibold text-base-50 mb-3\">{{ __('Recent Results') }}</h3>\n            <livewire:healthcheck-result-table :healthcheckId=\"$healthcheck->id\" wire:key=\"result-table-{{ $healthcheck->id }}\" />\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/healthchecks/resources/views/livewire/healthcheck-form.blade.php",
    "content": "@props(['updating' => false, 'inline' => false])\n<div>\n    @if (!$inline)\n        <x-slot name=\"header\">\n            <x-page-header :title=\"$updating\n                ? __('Edit Healthcheck - :domain', ['domain' => $healthcheck->domain])\n                : __('Add Healthcheck')\" :back=\"$updating\n                ? route('healthchecks.view', ['healthcheck' => $healthcheck])\n                : route('healthchecks.index')\">\n            </x-page-header>\n        </x-slot>\n    @endif\n\n    <form wire:submit=\"save\">\n        <div class=\"max-w-7xl mx-auto\">\n            <x-card>\n                <div class=\"flex flex-col gap-4\">\n                    @if (!$inline)\n                        <x-form.checkbox field=\"form.enabled\" name=\"Enabled\"\n                            description=\"Enable or disable this healthcheck\" />\n                    @endif\n\n                    <x-form.text field=\"form.domain\" name=\"URL\"\n                        description=\"URL of your service (e.g. https://govigilant.io)\" />\n\n                    <x-form.select field=\"form.interval\" name=\"Interval\"\n                        description=\"Choose how often this healthcheck should run\">\n                        @foreach (config('healthchecks.intervals') as $interval => $label)\n                            <option value=\"{{ $interval }}\">@lang($label)</option>\n                        @endforeach\n                    </x-form.select>\n\n                    <div x-data=\"{\n                        selectedType: $wire.entangle('form.type'),\n                        customizeEndpoint: {{ $healthcheck?->type?->value === 'endpoint' || ($healthcheck->endpoint !== null && $healthcheck->endpoint !== $healthcheck->type->endpoint()) ? 'true' : 'false' }},\n                        showAll: false,\n                        searchQuery: '',\n                        totalPlatforms: {{ count(\\Vigilant\\Healthchecks\\Enums\\Type::cases()) }},\n                        platformEndpoints: {\n                            @foreach (\\Vigilant\\Healthchecks\\Enums\\Type::cases() as $type)\n                                '{{ $type->value }}': {{ $type->endpoint() ? \"'\" . $type->endpoint() . \"'\" : \"''\" }}, @endforeach\n                        },\n                        get hasMore() {\n                            return this.totalPlatforms > 12;\n                        }\n                    }\" x-init=\"$watch('selectedType', value => {\n                        if (platformEndpoints[value] !== undefined) {\n                            $wire.set('form.endpoint', platformEndpoints[value]);\n                        }\n                    })\">\n                        <div>\n                            <div class=\"flex items-start justify-between gap-4 mb-3\">\n                                <div class=\"flex-1\">\n                                    <label class=\"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1\">\n                                        @lang('Platform')\n                                    </label>\n                                    <p class=\"text-sm text-gray-600 dark:text-gray-400\">\n                                        @lang('Select your platform, if you have not installed the Vigilant healthcheck module then select \"Endpoint\".')\n                                    </p>\n                                </div>\n                                <div class=\"w-64\">\n                                    <input type=\"text\" x-model=\"searchQuery\" placeholder=\"Search platforms...\"\n                                        class=\"block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm\" />\n                                </div>\n                            </div>\n\n                            <div class=\"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3\">\n                                @foreach (\\Vigilant\\Healthchecks\\Enums\\Type::cases() as $type)\n                                    <button type=\"button\" @click=\"selectedType = '{{ $type->value }}'\"\n                                        x-show=\"searchQuery === '' || '{{ strtolower($type->label()) }}'.includes(searchQuery.toLowerCase())\"\n                                        x-transition\n                                        class=\"relative flex flex-col items-center justify-center p-4 rounded-lg border transition-all hover:shadow-md\"\n                                        :class=\"selectedType === '{{ $type->value }}'\n                                            ?\n                                            'border-indigo-500 bg-indigo-100 dark:bg-indigo-500/20 dark:border-indigo-400' :\n                                            'border-gray-200 dark:border-gray-700 hover:border-indigo-300 dark:hover:border-indigo-600 bg-gray-50 dark:bg-gray-800/50'\"\n                                        x-data=\"{ index: {{ $loop->index }} }\"\n                                        x-show.transition=\"(showAll || index < 12) && (searchQuery === '' || '{{ strtolower($type->label()) }}'.includes(searchQuery.toLowerCase()))\">\n                                        <x-icon name=\"{{ $type->icon() }}\" class=\"w-8 h-8 mb-2 transition-colors\"\n                                            x-bind:class=\"selectedType === '{{ $type->value }}' ?\n                                                'text-indigo-700 dark:text-indigo-300' :\n                                                'text-gray-700 dark:text-gray-300'\" />\n                                        <span class=\"text-xs font-semibold transition-colors text-center\"\n                                            x-bind:class=\"selectedType === '{{ $type->value }}' ?\n                                                'text-indigo-700 dark:text-indigo-300' :\n                                                'text-gray-700 dark:text-gray-300'\">\n                                            {{ $type->label() }}\n                                        </span>\n                                        <x-icon name=\"heroicon-s-check-circle\"\n                                            class=\"absolute top-2 right-2 w-5 h-5 text-indigo-700 dark:text-indigo-300 transition-opacity\"\n                                            x-bind:class=\"selectedType === '{{ $type->value }}' ? 'opacity-100' : 'opacity-0'\" />\n                                    </button>\n                                @endforeach\n                            </div>\n\n                            <div x-show=\"hasMore && !showAll\" class=\"mt-3 text-center\">\n                                <button type=\"button\" @click=\"showAll = true\"\n                                    class=\"inline-flex items-center px-4 py-2 text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300\">\n                                    <span x-text=\"`Show ${totalPlatforms - 12} more platforms`\"></span>\n                                    <x-icon name=\"heroicon-s-chevron-down\" class=\"ml-1 w-4 h-4\" />\n                                </button>\n                            </div>\n\n                            <div x-show=\"showAll && hasMore\" class=\"mt-3 text-center\">\n                                <button type=\"button\" @click=\"showAll = false\"\n                                    class=\"inline-flex items-center px-4 py-2 text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300\">\n                                    @lang('Show less')\n                                    <x-icon name=\"heroicon-s-chevron-up\" class=\"ml-1 w-4 h-4\" />\n                                </button>\n                            </div>\n                        </div>\n\n                        <div class=\"mt-4 min-h-[88px]\">\n                            <div x-show=\"selectedType === 'endpoint'\">\n                                <x-form.text field=\"form.endpoint\" name=\"Endpoint\" :live=\"false\"\n                                    description=\"URL path to check (e.g., /health). Must return HTTP 200 status for a successful check.\" />\n                            </div>\n\n                            <div x-show=\"selectedType !== 'endpoint'\">\n                                <div class=\"flex items-center justify-between\">\n                                    <div class=\"flex-1\">\n                                        <label class=\"block text-sm font-medium text-gray-700 dark:text-gray-300\">\n                                            @lang('Customize endpoint')\n                                        </label>\n                                        <p class=\"text-sm text-gray-600 dark:text-gray-400\">\n                                            @lang('Override the default endpoint path for this platform')\n                                        </p>\n                                    </div>\n                                    <button type=\"button\" @click=\"customizeEndpoint = !customizeEndpoint\"\n                                        class=\"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2\"\n                                        :class=\"customizeEndpoint ? 'bg-indigo-600' : 'bg-gray-200 dark:bg-gray-700'\"\n                                        role=\"switch\" :aria-checked=\"customizeEndpoint\">\n                                        <span\n                                            class=\"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out\"\n                                            :class=\"customizeEndpoint ? 'translate-x-5' : 'translate-x-0'\"></span>\n                                    </button>\n                                </div>\n\n                                <div x-show=\"customizeEndpoint\" x-transition class=\"mt-4\">\n                                    <x-form.text field=\"form.endpoint\" name=\"Endpoint\" :live=\"false\"\n                                        description=\"Custom endpoint path (leave empty to use default)\" />\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n\n                    @if (!$inline)\n                        <div class=\"flex justify-end gap-4 items-center\">\n                            <x-form.submit-button dusk=\"submit-button\" wire:loading.attr=\"disabled\" :submitText=\"$updating ? 'Save' : 'Create'\" />\n                        </div>\n                    @endif\n                </div>\n            </x-card>\n        </div>\n    </form>\n</div>\n"
  },
  {
    "path": "packages/healthchecks/resources/views/livewire/healthcheck-setup.blade.php",
    "content": "<div>\n    <x-slot name=\"header\">\n        <x-page-header :title=\"__('Setup Healthcheck - :domain', ['domain' => $healthcheck->domain])\" :back=\"route('healthchecks.view', ['healthcheck' => $healthcheck])\">\n        </x-page-header>\n    </x-slot>\n\n    <div class=\"max-w-7xl mx-auto\">\n        <x-card>\n            <div class=\"flex flex-col gap-6\">\n                @if ($isNew)\n                    <div>\n                        <h3 class=\"text-lg font-semibold mb-2 text-gray-900 dark:text-gray-100\">\n                            {{ __('Healthcheck Created Successfully!') }}</h3>\n                        <p class=\"text-gray-600 dark:text-gray-400\">\n                            {{ __('Your healthcheck has been created. Follow the instructions below to integrate it with your platform.') }}\n                        </p>\n                    </div>\n                @endif\n\n                <div class=\"{{ $isNew ? 'border-t border-gray-200 dark:border-gray-700 pt-6' : '' }}\">\n                    <h4 class=\"text-md font-semibold mb-3 text-gray-900 dark:text-gray-100\">\n                        {{ __('Integration Instructions') }}</h4>\n\n                    <div class=\"space-y-4\">\n                        <livewire:healthcheck-token-editor :healthcheck=\"$healthcheck\" :key=\"'healthcheck-token-editor-' . $healthcheck->getKey()\" />\n\n                        @includeIf('healthchecks::platforms.' . $healthcheck->type->value, ['healthcheck' => $healthcheck])\n                    </div>\n                </div>\n\n                <div class=\"flex justify-end gap-4 border-t border-gray-200 dark:border-gray-700 pt-6\">\n                    <a href=\"{{ route('healthchecks.index') }}\"\n                        class=\"px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium\">\n                        {{ __('Go to Healthchecks') }}\n                    </a>\n                </div>\n            </div>\n        </x-card>\n    </div>\n\n</div>\n"
  },
  {
    "path": "packages/healthchecks/resources/views/livewire/healthcheck-token-editor.blade.php",
    "content": "<div>\n    <label class=\"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2\">\n        {{ __('Your Healthcheck Token') }}\n    </label>\n\n    @if ($healthcheck->type->generatesOwnToken())\n        <form wire:submit=\"save\" class=\"space-y-3\"\n            x-data=\"{\n                copied: false,\n                copy() {\n                    navigator.clipboard.writeText($refs.tokenInput.value).then(() => {\n                        this.copied = true;\n                        setTimeout(() => this.copied = false, 2000);\n                    });\n                }\n            }\">\n            <div class=\"flex items-center gap-2\">\n                <input type=\"text\" wire:model.defer=\"token\" x-ref=\"tokenInput\"\n                    class=\"flex-1 px-3 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-md font-mono text-sm text-gray-900 dark:text-gray-100\"\n                    autocomplete=\"off\" />\n                <button type=\"button\" @click=\"copy()\"\n                    class=\"px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium transition-colors duration-200\">\n                    <span x-text=\"copied ? '{{ __('Copied!') }}' : '{{ __('Copy') }}'\"></span>\n                </button>\n            </div>\n            <p class=\"text-sm text-gray-600 dark:text-gray-400\">\n                {{ __('Enter the token generated by :platform and save it here to authenticate healthchecks.', ['platform' => $healthcheck->type->label()]) }}\n            </p>\n            <div class=\"flex justify-end\">\n                <button type=\"submit\" wire:loading.attr=\"disabled\"\n                    class=\"px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-md text-sm font-medium\">\n                    <span wire:loading.remove>{{ __('Save Token') }}</span>\n                    <span wire:loading>{{ __('Saving...') }}</span>\n                </button>\n            </div>\n        </form>\n    @else\n        <div class=\"space-y-2\"\n            x-data=\"{\n                copied: false,\n                copy() {\n                    navigator.clipboard.writeText($refs.tokenInput.value).then(() => {\n                        this.copied = true;\n                        setTimeout(() => this.copied = false, 2000);\n                    });\n                }\n            }\">\n            <div class=\"flex items-center gap-2\">\n                <input type=\"text\" readonly value=\"{{ $healthcheck->token }}\" x-ref=\"tokenInput\"\n                    class=\"flex-1 px-3 py-2 bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md font-mono text-sm text-gray-900 dark:text-gray-100\" />\n                <button type=\"button\" @click=\"copy()\"\n                    class=\"px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium transition-colors duration-200\">\n                    <span x-text=\"copied ? '{{ __('Copied!') }}' : '{{ __('Copy') }}'\"></span>\n                </button>\n            </div>\n            <p class=\"text-sm text-gray-600 dark:text-gray-400\">\n                {{ __('This token is generated by Vigilant. Copy it into your platform configuration to enable healthchecks.') }}\n            </p>\n        </div>\n    @endif\n</div>\n"
  },
  {
    "path": "packages/healthchecks/resources/views/livewire/healthchecks.blade.php",
    "content": "<div>\n    <x-slot name=\"header\">\n        <x-page-header title=\"Healthchecks\">\n            <x-frontend::page-header.actions>\n                <x-create-button dusk=\"healthcheck-add-button\" :href=\"route('healthchecks.create')\" model=\"Vigilant\\Healthchecks\\Models\\Healthcheck\">\n                    @lang('Add Healthcheck')\n                </x-create-button>\n            </x-frontend::page-header.actions>\n            <x-frontend::page-header.mobile-actions>\n                <x-create-button-dropdown :href=\"route('healthchecks.create')\" model=\"Vigilant\\Healthchecks\\Models\\Healthcheck\">\n                    @lang('Add Healthcheck')\n                </x-create-button-dropdown>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    @if ($hasHealthchecks)\n        <livewire:healthcheck-table />\n    @else\n        <x-healthchecks::empty-states.healthchecks />\n    @endif\n</div>\n"
  },
  {
    "path": "packages/healthchecks/resources/views/platforms/drupal.blade.php",
    "content": "<div class=\"bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4\">\n    <h5 class=\"font-semibold text-blue-900 dark:text-blue-100 mb-2\">{{ __('Setup your Drupal healthcheck') }}</h5>\n    <ol class=\"list-decimal list-inside space-y-2 text-sm text-blue-800 dark:text-blue-200\">\n        <li><a class=\"text-blue-600 dark:text-blue-400 underline\" target=\"_blank\"\n                href=\"https://github.com/govigilant/drupal-healthchecks\">{{ __('Install the Drupal healthcheck module') }}</a>\n        </li>\n        <li>{{ __('In your Drupal site go to Configuration > Vigilant Healthchecks') }}</li>\n        <li>{{ __('Paste the token above in your Drupal site') }}</li>\n    </ol>\n</div>\n"
  },
  {
    "path": "packages/healthchecks/resources/views/platforms/endpoint.blade.php",
    "content": "<div class=\"space-y-4\">\n    <div class=\"bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4\">\n        <h5 class=\"font-semibold text-blue-900 dark:text-blue-100 mb-2\">{{ __('Setup Steps') }}</h5>\n        <ol class=\"list-decimal list-inside space-y-2 text-sm text-blue-800 dark:text-blue-200\">\n            <li>{{ __('Copy the token above') }}</li>\n            <li>{{ __('Add this token to your platform\\'s configuration') }}</li>\n            <li>{{ __('Ensure your endpoint :endpoint returns an HTTP 200 status', ['endpoint' => $healthcheck->endpoint]) }}\n            </li>\n            <li>{{ __('The healthcheck will run automatically at the configured interval') }}</li>\n        </ol>\n    </div>\n\n    <div class=\"bg-gray-50 dark:bg-gray-800 rounded-lg p-4\">\n        <h5 class=\"font-semibold text-gray-900 dark:text-gray-100 mb-2\">{{ __('Endpoint Details') }}</h5>\n        <div class=\"space-y-2 text-sm\">\n            <div>\n                <span class=\"font-medium text-gray-700 dark:text-gray-300\">{{ __('Domain:') }}</span>\n                <span class=\"text-gray-600 dark:text-gray-400\">{{ $healthcheck->domain }}</span>\n            </div>\n            <div>\n                <span class=\"font-medium text-gray-700 dark:text-gray-300\">{{ __('Endpoint:') }}</span>\n                <span class=\"text-gray-600 dark:text-gray-400\">{{ $healthcheck->endpoint }}</span>\n            </div>\n            <div>\n                <span class=\"font-medium text-gray-700 dark:text-gray-300\">{{ __('Check Interval:') }}</span>\n                <span class=\"text-gray-600 dark:text-gray-400\">{{ __('Every :interval minutes', ['interval' => $healthcheck->interval]) }}</span>\n            </div>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/healthchecks/resources/views/platforms/joomla.blade.php",
    "content": "<div class=\"bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4\">\n    <h5 class=\"font-semibold text-blue-900 dark:text-blue-100 mb-2\">{{ __('Setup your Joomla healthcheck') }}</h5>\n    <ol class=\"list-decimal list-inside space-y-2 text-sm text-blue-800 dark:text-blue-200\">\n        <li><a class=\"text-blue-600 dark:text-blue-400 underline\" target=\"_blank\"\n                href=\"https://github.com/govigilant/joomla-healthchecks\">{{ __('Install the Joomla healthcheck module') }}</a>\n        </li>\n        <li>{{ __('In your administrator go to System > Plugins > Vigilant Healthchecks') }}</li>\n        <li>{{ __('Paste the token above in your Joomla site') }}</li>\n    </ol>\n</div>\n"
  },
  {
    "path": "packages/healthchecks/resources/views/platforms/laravel.blade.php",
    "content": "<div class=\"bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4\">\n    <h5 class=\"font-semibold text-blue-900 dark:text-blue-100 mb-2\">{{ __('Laravel Setup Steps') }}</h5>\n    <ol class=\"list-decimal list-inside space-y-2 text-sm text-blue-800 dark:text-blue-200\">\n        <li><a class=\"text-blue-600 dark:text-blue-400 underline\" target=\"_blank\"\n                href=\"https://github.com/govigilant/laravel-healthchecks\">{{ __('Install the Laravel healthcheck package') }}</a>\n        </li>\n        <li>{{ __('Configure the token in your environment file') }}</li>\n    </ol>\n</div>\n"
  },
  {
    "path": "packages/healthchecks/resources/views/platforms/magento.blade.php",
    "content": "<div class=\"bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4\">\n    <h5 class=\"font-semibold text-blue-900 dark:text-blue-100 mb-2\">{{ __('Setup your Magento 2 healthcheck') }}</h5>\n    <ol class=\"list-decimal list-inside space-y-2 text-sm text-blue-800 dark:text-blue-200\">\n        <li><a class=\"text-blue-600 dark:text-blue-400 underline\" target=\"_blank\"\n                href=\"https://github.com/govigilant/magento2-healthchecks\">{{ __('Install the Magento 2 healthcheck module') }}</a>\n        </li>\n        <li>{{ __('In your Magento 2 backend, go to System > Integrations') }}</li>\n        <li>{{ __('Create an integration with permissions for the \"Health Endpoint\"') }}</li>\n        <li>{{ __('Activate the integration and paste the access token here') }}</li>\n    </ol>\n</div>\n"
  },
  {
    "path": "packages/healthchecks/resources/views/platforms/statamic.blade.php",
    "content": "<div class=\"bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4\">\n    <h5 class=\"font-semibold text-blue-900 dark:text-blue-100 mb-2\">{{ __('Statamic Setup Steps') }}</h5>\n    <ol class=\"list-decimal list-inside space-y-2 text-sm text-blue-800 dark:text-blue-200\">\n        <li><a class=\"text-blue-600 dark:text-blue-400 underline\" target=\"_blank\"\n                href=\"https://github.com/govigilant/statamic-healthchecks\">{{ __('Install the Statamic healthcheck package') }}</a>\n        </li>\n        <li>{{ __('Configure the token in your environment file') }}</li>\n    </ol>\n</div>\n"
  },
  {
    "path": "packages/healthchecks/resources/views/platforms/wordpress.blade.php",
    "content": "<div class=\"bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4\">\n    <h5 class=\"font-semibold text-blue-900 dark:text-blue-100 mb-2\">{{ __('Setup your WordPress healthcheck') }}</h5>\n    <ol class=\"list-decimal list-inside space-y-2 text-sm text-blue-800 dark:text-blue-200\">\n        <li><a class=\"text-blue-600 dark:text-blue-400 underline\" target=\"_blank\"\n                href=\"https://wordpress.org/plugins/vigilant-healthchecks/\">{{ __('Install the WordPress healthcheck module') }}</a>\n        </li>\n        <li>{{ __('In your wp-admin go to Settings > Vigilant Healthchecks') }}</li>\n        <li>{{ __('Paste the token above in your WordPress site') }}</li>\n    </ol>\n</div>\n"
  },
  {
    "path": "packages/healthchecks/routes/api.php",
    "content": "<?php\n\n// API routes for healthchecks package\n"
  },
  {
    "path": "packages/healthchecks/routes/web.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\Healthchecks\\Http\\Controllers\\HealthcheckController;\nuse Vigilant\\Healthchecks\\Livewire\\HealthcheckForm;\nuse Vigilant\\Healthchecks\\Livewire\\Healthchecks;\nuse Vigilant\\Healthchecks\\Livewire\\HealthcheckSetup;\n\nRoute::prefix('healthchecks')\n    ->middleware('can:use-healthchecks')\n    ->group(function (): void {\n        Route::get('/', Healthchecks::class)->name('healthchecks.index');\n        Route::get('/create', HealthcheckForm::class)->name('healthchecks.create');\n        Route::get('/{healthcheck}', [HealthcheckController::class, 'index'])->name('healthchecks.view');\n        Route::get('/{healthcheck}/setup', HealthcheckSetup::class)->name('healthchecks.setup');\n        Route::delete('/{healthcheck}', [HealthcheckController::class, 'delete'])->name('healthchecks.delete')->can('delete,healthcheck');\n        Route::get('/{healthcheck}/edit', HealthcheckForm::class)->name('healthchecks.edit');\n    });\n"
  },
  {
    "path": "packages/healthchecks/src/Actions/AggregateMetrics.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Actions;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\DB;\nuse Vigilant\\Healthchecks\\Models\\Metric;\n\nclass AggregateMetrics\n{\n    public function handle(): void\n    {\n        $latestAggregatableHour = $this->latestAggregatableHourStart();\n\n        if ($latestAggregatableHour === null) {\n            return;\n        }\n\n        $hourBuckets = $this->hourBuckets($latestAggregatableHour);\n\n        if ($hourBuckets->isEmpty()) {\n            return;\n        }\n\n        foreach ($hourBuckets as $hourStart) {\n            $this->aggregateHour($hourStart);\n        }\n    }\n\n    protected function hourBuckets(Carbon $latestAggregatableHour): Collection\n    {\n        $hours = collect();\n        $upperBound = $latestAggregatableHour->copy()->addHour();\n\n        Metric::query()\n            ->select('id', 'created_at')\n            ->whereNotNull('created_at')\n            ->where('created_at', '<', $upperBound)\n            ->orderBy('id')\n            ->chunkById(1000, function ($metrics) use (&$hours): void {\n                foreach ($metrics as $metric) {\n                    $createdAt = $metric->created_at;\n\n                    if ($createdAt === null) {\n                        continue;\n                    }\n\n                    $hourStart = $createdAt->copy()->startOfHour();\n                    $hours->put((int) $hourStart->timestamp, $hourStart);\n                }\n            });\n\n        return $hours\n            ->sortKeys()\n            ->values();\n    }\n\n    protected function aggregateHour(Carbon $hourStart): void\n    {\n        $hourEnd = $hourStart->copy()->addHour();\n\n        $metrics = Metric::query()\n            ->whereNotNull('created_at')\n            ->where('created_at', '>=', $hourStart)\n            ->where('created_at', '<', $hourEnd)\n            ->orderBy('healthcheck_id')\n            ->orderBy('key')\n            ->orderBy('unit')\n            ->orderBy('created_at')\n            ->orderBy('id')\n            ->get();\n\n        if ($metrics->isEmpty()) {\n            return;\n        }\n\n        $metrics\n            ->groupBy(function (Metric $metric): string {\n                $unit = $metric->unit ?? '__null__';\n\n                return implode('|', [\n                    $metric->healthcheck_id,\n                    $metric->key,\n                    $unit,\n                ]);\n            })\n            ->each(function (Collection $group): void {\n                $this->aggregateGroup($group);\n            });\n    }\n\n    protected function aggregateGroup(Collection $metrics): void\n    {\n        if ($metrics->count() <= 1) {\n            return;\n        }\n\n        $sorted = $metrics->sortBy(function (Metric $metric): string {\n            $timestamp = $metric->created_at?->format('YmdHisu') ?? '000000000000000000';\n\n            return $timestamp.'|'.$metric->id;\n        })->values();\n\n        $average = $sorted->avg(fn (Metric $metric): float => (float) $metric->value);\n\n        if ($average === null) {\n            return;\n        }\n\n        /** @var Metric $first */\n        $first = $sorted->shift();\n\n        $idsToDelete = $sorted->pluck('id')->filter()->all();\n\n        DB::transaction(function () use ($first, $average, $idsToDelete): void {\n            $first->forceFill([\n                'value' => round($average, 2),\n            ])->save();\n\n            if ($idsToDelete !== []) {\n                Metric::query()\n                    ->whereIn('id', $idsToDelete)\n                    ->delete();\n            }\n        });\n    }\n\n    protected function latestAggregatableHourStart(): ?Carbon\n    {\n        $now = Carbon::now();\n        $start = $now->copy()->subHours(2)->startOfHour();\n\n        if ($start->greaterThanOrEqualTo($now)) {\n            return null;\n        }\n\n        return $start;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Actions/CheckHealth.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Actions;\n\nuse Exception;\nuse Vigilant\\Healthchecks\\Enums\\Status;\nuse Vigilant\\Healthchecks\\Jobs\\CheckResultJob;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass CheckHealth\n{\n    public function check(Healthcheck $healthcheck): void\n    {\n        $runId = null;\n\n        try {\n            $checker = $healthcheck->type->checker();\n\n            $runId = $checker->check($healthcheck);\n        } catch (Exception $e) {\n            logger()->error('Healthcheck failed for Healthcheck ID '.$healthcheck->id.': '.$e->getMessage());\n\n            if (app()->isLocal()) {\n                throw $e;\n            }\n\n            $healthcheck->update([\n                'status' => Status::Unhealthy,\n            ]);\n        }\n\n        $healthcheck->update([\n            'next_check_at' => now()->addSeconds($healthcheck->interval),\n            'last_check_at' => now(),\n        ]);\n\n        if ($runId !== null) {\n            CheckResultJob::dispatch($healthcheck, $runId);\n        }\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Actions/CheckMetric.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Actions;\n\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\nuse Vigilant\\Healthchecks\\Models\\Metric;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricIncreaseTimeframeCondition;\nuse Vigilant\\Healthchecks\\Notifications\\DiskUsageNotification;\nuse Vigilant\\Healthchecks\\Notifications\\MetricIncreasingNotification;\nuse Vigilant\\Healthchecks\\Notifications\\MetricNotification;\nuse Vigilant\\Healthchecks\\Notifications\\MetricSpikeNotification;\n\nclass CheckMetric\n{\n    public function check(Healthcheck $healthcheck, int $runId): void\n    {\n        /** @var Collection<int, Metric> $metrics */\n        $metrics = $healthcheck->metrics()\n            ->where('run_id', '=', $runId)\n            ->get();\n\n        if ($metrics->isEmpty()) {\n            return;\n        }\n\n        foreach ($metrics as $metric) {\n            MetricNotification::notify($metric);\n        }\n\n        $this->checkIncreasingMetrics($healthcheck, $metrics);\n        $this->checkDiskUsage($healthcheck, $metrics);\n    }\n\n    protected function checkIncreasingMetrics(Healthcheck $healthcheck, Collection $metrics): void\n    {\n        $metricsByKey = $metrics->groupBy('key');\n\n        foreach ($metricsByKey as $key => $keyMetrics) {\n            $currentMetric = $keyMetrics->first();\n            $increaseData = $this->calculateMetricIncrease($healthcheck, $key, $currentMetric);\n\n            if ($increaseData !== null) {\n                if (isset($increaseData['detection_type']) && $increaseData['detection_type'] === 'sudden_spike') {\n                    MetricSpikeNotification::notify($currentMetric, $increaseData);\n                } else {\n                    MetricIncreasingNotification::notify($currentMetric, $increaseData);\n                }\n            }\n        }\n    }\n\n    public function calculateMetricIncrease(Healthcheck $healthcheck, string $key, Metric $currentMetric): ?array\n    {\n        /** @var Collection<int, Metric> $historicalMetrics */\n        $historicalMetrics = $healthcheck->metrics()\n            ->where('key', '=', $key)\n            ->where('created_at', '<=', $currentMetric->created_at)\n            ->orderBy('created_at', 'desc')\n            ->limit(60)\n            ->get();\n\n        if ($historicalMetrics->count() < 2 || $currentMetric->created_at === null) {\n            return null;\n        }\n\n        $currentValue = $currentMetric->value;\n\n        // Spike detection\n        $recentMetrics = $historicalMetrics->take(5);\n        if ($recentMetrics->count() >= 3) {\n            $spikeResult = $this->detectSpike($recentMetrics, $currentValue, $currentMetric);\n            if ($spikeResult !== null) {\n                return $spikeResult;\n            }\n        }\n\n        $intervalResults = [];\n        $intervals = MetricIncreaseTimeframeCondition::INTERVALS;\n\n        foreach ($intervals as $index => $interval) {\n            $minBound = $interval;\n            $maxBound = $intervals[$index + 1] ?? null;\n\n            $baselineMetric = $this->findBaselineMetricForRange(\n                $historicalMetrics,\n                $currentMetric,\n                $minBound,\n                $maxBound\n            );\n\n            if ($baselineMetric === null) {\n                continue;\n            }\n\n            $oldValue = $baselineMetric->value;\n\n            if ($oldValue == 0) {\n                continue;\n            }\n\n            $percentIncrease = (($currentValue - $oldValue) / $oldValue) * 100;\n\n            if ($percentIncrease <= 0) {\n                continue;\n            }\n\n            $intervalResults[] = [\n                'key' => $key,\n                'old_value' => $oldValue,\n                'new_value' => $currentValue,\n                'unit' => $currentMetric->unit,\n                'percent_increase' => $percentIncrease,\n                'timeframe_minutes' => $interval,\n                'sample_size' => $historicalMetrics->count(),\n                'detection_type' => 'long_term_trend',\n            ];\n        }\n\n        if ($intervalResults === []) {\n            return null;\n        }\n\n        return $intervalResults;\n    }\n\n    protected function findBaselineMetricForRange(\n        Collection $historicalMetrics,\n        Metric $currentMetric,\n        int $minBoundary,\n        ?int $maxBoundary\n    ): ?Metric {\n        if ($currentMetric->created_at === null) {\n            return null;\n        }\n\n        return $historicalMetrics->first(function (Metric $metric) use ($currentMetric, $minBoundary, $maxBoundary) {\n            if ($metric->is($currentMetric) || $metric->created_at === null) {\n                return false;\n            }\n\n            $difference = $currentMetric->created_at->diffInMinutes($metric->created_at, true);\n\n            if ($difference < $minBoundary) {\n                return false;\n            }\n\n            if ($maxBoundary !== null && $difference >= $maxBoundary) {\n                return false;\n            }\n\n            return true;\n        });\n    }\n\n    public function detectSpike(Collection $recentMetrics, float $currentValue, Metric $currentMetric): ?array\n    {\n        /** @var Metric $recentOldest */\n        $recentOldest = $recentMetrics->last();\n        $recentOldestValue = $recentOldest->value;\n\n        if ($recentOldestValue == 0) {\n            return null;\n        }\n\n        $percentIncrease = (($currentValue - $recentOldestValue) / $recentOldestValue) * 100;\n\n        // Spike threshold: 50% increase\n        if ($percentIncrease < 50) {\n            return null;\n        }\n\n        $timeframeMinutes = $currentMetric->created_at?->diffInMinutes($recentOldest->created_at, true) ?? 0;\n\n        return [\n            'key' => $currentMetric->key,\n            'old_value' => $recentOldestValue,\n            'new_value' => $currentValue,\n            'unit' => $currentMetric->unit,\n            'percent_increase' => $percentIncrease,\n            'timeframe_minutes' => $timeframeMinutes,\n            'sample_size' => $recentMetrics->count(),\n            'detection_type' => 'sudden_spike',\n        ];\n    }\n\n    protected function checkDiskUsage(Healthcheck $healthcheck, Collection $metrics): void\n    {\n        /** @var ?Metric $diskMetric */\n        $diskMetric = $metrics->firstWhere('key', 'disk_usage');\n\n        if (! $diskMetric || $diskMetric->unit !== '%') {\n            return;\n        }\n\n        /** @var Collection<int, Metric> $historicalMetrics */\n        $historicalMetrics = $healthcheck->metrics()\n            ->where('key', 'disk_usage')\n            ->where('created_at', '<=', $diskMetric->created_at)\n            ->orderBy('created_at', 'desc')\n            ->limit(60)\n            ->get();\n\n        $currentUsage = $diskMetric->value;\n        /** @var ?Metric $oldestMetric */\n        $oldestMetric = $historicalMetrics->last();\n\n        if ($oldestMetric === null) {\n            return;\n        }\n\n        $oldestUsage = $oldestMetric->value;\n\n        $timeframeHours = $oldestMetric->created_at?->diffInHours($diskMetric->created_at, true) ?? 0;\n\n        if ($timeframeHours == 0) {\n            return;\n        }\n\n        $velocity = ($currentUsage - $oldestUsage) / $timeframeHours;\n\n        if ($velocity <= 0) {\n            return;\n        }\n\n        $remainingSpace = 100 - $currentUsage;\n        $hoursUntilFull = $remainingSpace / $velocity;\n\n        if ($hoursUntilFull < 0) {\n            return;\n        }\n\n        $estimatedFullAt = now()->addHours($hoursUntilFull);\n\n        DiskUsageNotification::notify(\n            $healthcheck,\n            $currentUsage,\n            $velocity,\n            $hoursUntilFull,\n            $estimatedFullAt\n        );\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Actions/CheckResult.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Actions;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Vigilant\\Healthchecks\\Enums\\Status;\nuse Vigilant\\Healthchecks\\Jobs\\CheckMetricJob;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\nuse Vigilant\\Healthchecks\\Models\\Result;\nuse Vigilant\\Healthchecks\\Notifications\\HealthCheckFailedNotification;\n\nclass CheckResult\n{\n    public function check(Healthcheck $healthcheck, int $runId): void\n    {\n        /** @var Collection<int, Result> $results */\n        $results = $healthcheck->results()->get();\n\n        $overallStatus = Status::Healthy;\n        $unhealthy = false;\n        $hasWarning = false;\n\n        foreach ($results as $result) {\n            if ($result->status === Status::Unhealthy) {\n                $unhealthy = true;\n                break;\n            }\n            if ($result->status === Status::Warning) {\n                $hasWarning = true;\n            }\n        }\n\n        if ($unhealthy) {\n            $overallStatus = Status::Unhealthy;\n        } elseif ($hasWarning) {\n            $overallStatus = Status::Warning;\n        }\n\n        if ($unhealthy || $hasWarning) {\n            HealthCheckFailedNotification::notify($healthcheck, $runId);\n        }\n\n        $healthcheck->update(['status' => $overallStatus]);\n\n        CheckMetricJob::dispatch($healthcheck, $runId);\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Checks/Checker.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Checks;\n\nuse Closure;\nuse Illuminate\\Http\\Client\\ConnectionException;\nuse Illuminate\\Http\\Client\\PendingRequest;\nuse Illuminate\\Http\\Client\\Response;\nuse Illuminate\\Support\\Facades\\Http;\nuse RuntimeException;\nuse Vigilant\\Healthchecks\\Enums\\Status;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nabstract class Checker\n{\n    /** @return int runId */\n    abstract public function check(Healthcheck $healthcheck): int;\n\n    protected function generateRunId(Healthcheck $healthcheck): int\n    {\n        for ($i = 0; $i < 10; $i++) {\n            $candidate = rand(1, 100000);\n\n            $exists = $healthcheck->metrics()->where('run_id', '=', $candidate)->exists();\n\n            if (! $exists) {\n                return $candidate;\n            }\n        }\n\n        throw new RuntimeException('Could not generate unique run ID');\n    }\n\n    protected function persistResult(Healthcheck $healthcheck, string $key, Status $status, ?string $message = null, ?array $data = null): void\n    {\n        $attributes = [\n            'status' => $status,\n            'message' => $message,\n            'data' => $data,\n            'last_checked_at' => now(),\n        ];\n\n        if ($status === Status::Unhealthy) {\n            $attributes['last_unhealthy_at'] = now();\n        }\n\n        $healthcheck->results()->updateOrCreate(\n            ['key' => $key],\n            $attributes\n        );\n    }\n\n    /**\n     * @param  Closure(PendingRequest): Response  $callback\n     *\n     * @throws ConnectionException\n     */\n    protected function performHttpCall(Healthcheck $healthcheck, Closure $callback): Response\n    {\n        $maxAttempts = max((int) config('healthchecks.http_max_attempts', 2), 1);\n\n        for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {\n            try {\n                $request = Http::baseUrl($healthcheck->domain);\n\n                return $callback($request);\n            } catch (ConnectionException $e) {\n                if ($attempt === $maxAttempts) {\n                    throw $e;\n                }\n\n                sleep(1);\n            }\n        }\n\n        throw new RuntimeException('Unable to perform HTTP call.');\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Checks/Endpoint.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Checks;\n\nuse Illuminate\\Http\\Client\\ConnectionException;\nuse Illuminate\\Http\\Client\\PendingRequest;\nuse Illuminate\\Http\\Client\\Response;\nuse InvalidArgumentException;\nuse Vigilant\\Healthchecks\\Enums\\Status;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass Endpoint extends Checker\n{\n    public function check(Healthcheck $healthcheck): int\n    {\n        throw_if($healthcheck->endpoint === null, InvalidArgumentException::class, 'Healthcheck endpoint is not defined');\n\n        $timeout = config('healthchecks.http_timeout', 10);\n        $runId = $this->generateRunId($healthcheck);\n\n        try {\n            $response = $this->performHttpCall(\n                $healthcheck,\n                function (PendingRequest $request) use ($timeout, $healthcheck): Response {\n                    return $request\n                        ->timeout($timeout)\n                        ->get($healthcheck->endpoint);\n                }\n            );\n\n            $healthy = $response->ok();\n\n            $status = $healthy ? Status::Healthy : Status::Unhealthy;\n            $message = $healthy\n                ? __('Endpoint is reachable')\n                : __('Endpoint returned status code :code', ['code' => $response->status()]);\n\n            $this->persistResult($healthcheck, 'endpoint_check', $status, $message);\n        } catch (ConnectionException $e) {\n            $this->persistResult(\n                $healthcheck,\n                'connection',\n                Status::Unhealthy,\n                'Failed to connect to endpoint',\n                [\n                    'error' => $e->getMessage(),\n                ]\n            );\n        }\n\n        return $runId;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Checks/Module.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Checks;\n\nuse Illuminate\\Http\\Client\\ConnectionException;\nuse Illuminate\\Http\\Client\\PendingRequest;\nuse Illuminate\\Http\\Client\\Response;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Vigilant\\Healthchecks\\Enums\\Status;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass Module extends Checker\n{\n    public function check(Healthcheck $healthcheck): int\n    {\n        $runId = $this->generateRunId($healthcheck);\n\n        $endpoint = blank($healthcheck->endpoint) ? $healthcheck->type->endpoint() : $healthcheck->endpoint;\n\n        throw_if($endpoint === null, 'Endpoint is required');\n\n        try {\n            $response = $this->performHttpCall(\n                $healthcheck,\n                function (PendingRequest $request) use ($healthcheck, $endpoint): Response {\n                    return $request\n                        ->withToken($healthcheck->token)\n                        ->post($endpoint);\n                }\n            );\n        } catch (ConnectionException) {\n            $this->persistResult($healthcheck, 'connection', Status::Unhealthy, 'Could not connect');\n\n            return $runId;\n        }\n\n        if ($response->failed()) {\n            $this->persistResult($healthcheck, 'connection', Status::Unhealthy, 'Failed to check health, status: '.$response->status());\n\n            return $runId;\n        }\n\n        $this->persistResult($healthcheck, 'connection', Status::Healthy);\n\n        $checks = $response->json($healthcheck->type->checksResponseKey(), []);\n        $metrics = $response->json($healthcheck->type->metricsResponseKey(), []);\n\n        foreach ($checks as $check) {\n            $validator = Validator::make($check, [\n                'type' => ['required', 'string'],\n                'key' => ['nullable', 'string'],\n                'status' => ['required', 'string', 'in:healthy,unhealthy,failed'],\n                'message' => ['nullable', 'string'],\n            ]);\n\n            if ($validator->fails()) {\n                continue;\n            }\n\n            $status = Status::from($check['status']);\n\n            $key = $check['type'];\n\n            if (! blank($check['key'] ?? null)) {\n                $key .= '_'.$check['key'];\n            }\n\n            $this->persistResult($healthcheck, str($key)->limit(255)->toString(), $status, $check['message'] ?? null);\n        }\n\n        foreach ($metrics as $metric) {\n            $validator = Validator::make($metric, [\n                'type' => ['required', 'string'],\n                'value' => ['required', 'numeric'],\n                'unit' => ['required', 'string'],\n            ]);\n\n            if ($validator->fails()) {\n                continue;\n            }\n\n            $healthcheck->metrics()->create([\n                'run_id' => $runId,\n                'key' => $metric['type'],\n                'value' => $metric['value'],\n                'unit' => $metric['unit'],\n            ]);\n        }\n\n        return $runId;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Commands/AggregateMetricsCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Healthchecks\\Jobs\\AggregateMetricsJob;\n\nclass AggregateMetricsCommand extends Command\n{\n    protected $signature = 'healthchecks:aggregate-metrics';\n\n    protected $description = 'Aggregate historical healthcheck metrics per hour';\n\n    public function handle(): int\n    {\n        AggregateMetricsJob::dispatch();\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Commands/CheckHealthcheckCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Healthchecks\\Jobs\\CheckHealthcheckJob;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass CheckHealthcheckCommand extends Command\n{\n    protected $signature = 'healthchecks:check {healthcheckId}';\n\n    protected $description = 'Check healthcheck for a specific healthcheck';\n\n    public function handle(): int\n    {\n        /** @var int $healthcheckId */\n        $healthcheckId = $this->argument('healthcheckId');\n\n        /** @var Healthcheck $healthcheck */\n        $healthcheck = Healthcheck::query()->withoutGlobalScopes()->findOrFail($healthcheckId);\n\n        CheckHealthcheckJob::dispatch($healthcheck);\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Commands/ScheduleHealthchecksCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Vigilant\\Healthchecks\\Jobs\\CheckHealthcheckJob;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass ScheduleHealthchecksCommand extends Command\n{\n    protected $signature = 'healthchecks:schedule';\n\n    protected $description = 'Schedule Healthcheck Jobs';\n\n    public function handle(): int\n    {\n        Healthcheck::query()\n            ->withoutGlobalScopes()\n            ->where('enabled', '=', true)\n            ->where(function (Builder $builder): void {\n                $builder->where('next_check_at', '<=', now())\n                    ->orWhereNull('next_check_at');\n            })\n            ->get()\n            ->each(function (Healthcheck $healthcheck): void {\n                CheckHealthcheckJob::dispatch($healthcheck);\n            });\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Enums/Status.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Enums;\n\nenum Status: string\n{\n    case Healthy = 'healthy';\n    case Warning = 'warning';\n    case Unhealthy = 'unhealthy';\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Enums/Type.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Enums;\n\nuse Vigilant\\Healthchecks\\Checks\\Checker;\nuse Vigilant\\Healthchecks\\Checks\\Endpoint;\nuse Vigilant\\Healthchecks\\Checks\\Module;\n\nenum Type: string\n{\n    case Endpoint = 'endpoint';\n    case Laravel = 'laravel';\n    case Statamic = 'statamic';\n    case Magento = 'magento';\n    case Wordpress = 'wordpress';\n    case Joomla = 'joomla';\n    case Drupal = 'drupal';\n\n    public function label(): string\n    {\n        return match ($this) {\n            default => ucfirst($this->value),\n        };\n    }\n\n    public function icon(): string\n    {\n        return match ($this) {\n            self::Endpoint => 'phosphor-heartbeat',\n            self::Laravel => 'si-laravel',\n            self::Statamic => 'si-statamic',\n            self::Magento => 'bxl-magento',\n            self::Wordpress => 'si-wordpress',\n            self::Joomla => 'si-joomla',\n            self::Drupal => 'si-drupal',\n        };\n    }\n\n    public function endpoint(): ?string\n    {\n        return match ($this) {\n            self::Endpoint => null,\n            self::Magento => 'rest/V1/vigilant/health',\n            self::Wordpress => 'wp-json/vigilant/v1/health',\n            self::Joomla => 'index.php?option=io_govigilant&task=health.check',\n            self::Drupal => 'vigilant/health',\n            default => 'api/vigilant/health'\n        };\n    }\n\n    public function checker(): Checker\n    {\n        $class = match ($this) {\n            self::Endpoint => Endpoint::class,\n            default => Module::class,\n        };\n\n        return app($class);\n    }\n\n    public function generatesOwnToken(): bool\n    {\n        return match ($this) {\n            self::Magento => true,\n            default => false,\n        };\n    }\n\n    public function checksResponseKey(): string\n    {\n        return match ($this) {\n            self::Magento => '0',\n            default => 'checks',\n        };\n    }\n\n    public function metricsResponseKey(): string\n    {\n        return match ($this) {\n            self::Magento => '1',\n            default => 'metrics',\n        };\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Http/Controllers/HealthcheckController.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Http\\Controllers;\n\nuse Illuminate\\Routing\\Controller;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass HealthcheckController extends Controller\n{\n    use DisplaysAlerts;\n\n    public function index(Healthcheck $healthcheck): mixed\n    {\n        /** @var view-string $view */\n        $view = 'healthchecks::healthcheck.view';\n\n        return view($view, [\n            'healthcheck' => $healthcheck,\n        ]);\n    }\n\n    public function delete(Healthcheck $healthcheck): mixed\n    {\n        $healthcheck->delete();\n\n        $this->alert(\n            __('Deleted'),\n            __('Healthcheck was successfully deleted'),\n            AlertType::Success\n        );\n\n        return response()->redirectToRoute('healthchecks.index');\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Http/Livewire/Charts/MetricChart.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Http\\Livewire\\Charts;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Illuminate\\Support\\Str;\nuse Illuminate\\View\\View;\nuse Livewire\\Attributes\\Locked;\nuse Vigilant\\Frontend\\Http\\Livewire\\BaseChart;\nuse Vigilant\\Healthchecks\\Models\\Metric;\n\nclass MetricChart extends BaseChart\n{\n    #[Locked]\n    public int $healthcheckId = 0;\n\n    public int $height = 200;\n\n    public string $selectedKey = '';\n\n    public array $availableKeys = [];\n\n    public string $dateRange = 'week';\n\n    public function mount(array $data): void\n    {\n        Validator::make($data, [\n            'healthcheckId' => 'required',\n        ])->validate();\n\n        $this->healthcheckId = $data['healthcheckId'];\n\n        $this->availableKeys = $this->getAvailableKeys()->toArray();\n\n        if (! empty($this->availableKeys)) {\n            $this->selectedKey = $this->availableKeys[0];\n        }\n    }\n\n    public function setMetricKey(string $key): void\n    {\n        $this->selectedKey = $key;\n        $this->loadChart();\n    }\n\n    public function setDateRange(string $range): void\n    {\n        $this->dateRange = $range;\n        $this->loadChart();\n    }\n\n    protected function getDateRangeStart(): Carbon\n    {\n        return match ($this->dateRange) {\n            'hour' => now()->subHour(),\n            'day' => now()->subDay(),\n            'week' => now()->subWeek(),\n            'month' => now()->subMonth(),\n            '3months' => now()->subMonths(3),\n            '6months' => now()->subMonths(6),\n            default => now()->subWeek(),\n        };\n    }\n\n    protected function getDateRangeOptions(): array\n    {\n        return [\n            'hour' => 'Hour',\n            'day' => 'Day',\n            'week' => 'Week',\n            'month' => 'Month',\n            '3months' => '3 Months',\n            '6months' => '6 Months',\n        ];\n    }\n\n    protected function getAvailableKeys(): Collection\n    {\n        return Metric::query()\n            ->where('healthcheck_id', '=', $this->healthcheckId)\n            ->whereNotNull('key')\n            ->where('key', '!=', '')\n            ->selectRaw('`key`, COUNT(*) as count')\n            ->groupBy('key')\n            ->orderByDesc('count')\n            ->get()\n            ->pluck('key');\n    }\n\n    protected function points(): Collection\n    {\n        if (empty($this->selectedKey)) {\n            return collect();\n        }\n\n        return Metric::query()\n            ->where('healthcheck_id', '=', $this->healthcheckId)\n            ->where('key', '=', $this->selectedKey)\n            ->where('created_at', '>=', $this->getDateRangeStart())\n            ->orderBy('created_at', 'asc')\n            ->get();\n    }\n\n    public function data(): array\n    {\n        $points = $this->points();\n\n        if ($points->isEmpty()) {\n            return [\n                'type' => 'line',\n                'data' => [\n                    'labels' => [],\n                    'datasets' => [],\n                ],\n            ];\n        }\n\n        $labels = $points->pluck('created_at');\n        $data = $points->pluck('value');\n        $unit = $points->first()->unit ?? '';\n\n        $dateFormat = match ($this->dateRange) {\n            'hour' => 'H:i',\n            'day' => 'd/m H:i',\n            'week' => 'd/m H:i',\n            'month' => 'd/m',\n            '3months' => 'd/m',\n            '6months' => 'd/m',\n            default => 'd/m H:i',\n        };\n\n        $color = $this->getChartColor(0);\n\n        return [\n            'type' => 'line',\n            'data' => [\n                'labels' => $labels->map(fn (Carbon $carbon): string => teamTimezone($carbon)->format($dateFormat))->toArray(),\n                'datasets' => [\n                    [\n                        'label' => $this->selectedKey,\n                        'data' => $data->toArray(),\n                        'pointRadius' => 1,\n                        'pointHoverRadius' => 4,\n                        'borderWidth' => 2,\n                        'borderColor' => $color['border'],\n                        'backgroundColor' => $color['bg'],\n                        'fill' => true,\n                        'tension' => 0.4,\n                        'unit' => $unit,\n                    ],\n                ],\n            ],\n            'options' => [\n                'plugins' => [\n                    'legend' => [\n                        'display' => true,\n                    ],\n                    'tooltip' => [\n                        'enabled' => true,\n                    ],\n                ],\n                'scales' => [\n                    'y' => [\n                        'display' => true,\n                        'beginAtZero' => true,\n                    ],\n                    'x' => [\n                        'display' => true,\n                    ],\n                ],\n            ],\n        ];\n    }\n\n    protected function getIdentifier(): string\n    {\n        return Str::slug(get_class($this)).$this->healthcheckId;\n    }\n\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'healthchecks::livewire.charts.metric-chart';\n\n        return view($view, [\n            'identifier' => $this->getIdentifier(),\n            'height' => $this->height,\n            'availableKeys' => $this->getAvailableKeys(),\n            'dateRangeOptions' => $this->getDateRangeOptions(),\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Jobs/AggregateMetricsJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Healthchecks\\Actions\\AggregateMetrics;\n\nclass AggregateMetricsJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct()\n    {\n        $this->onQueue(config()->string('healthchecks.queue'));\n    }\n\n    public function handle(AggregateMetrics $aggregateMetrics): void\n    {\n        $aggregateMetrics->handle();\n    }\n\n    public function uniqueId(): string\n    {\n        return 'aggregate-metrics';\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Jobs/CheckHealthcheckJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Healthchecks\\Actions\\CheckHealth;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass CheckHealthcheckJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(public Healthcheck $healthcheck)\n    {\n        $this->onQueue(config()->string('healthchecks.queue'));\n    }\n\n    public function handle(CheckHealth $checkHealth, TeamService $teamService): void\n    {\n        $teamService->setTeamById($this->healthcheck->team_id);\n        $checkHealth->check($this->healthcheck);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->healthcheck->id;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Jobs/CheckMetricJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Healthchecks\\Actions\\CheckMetric;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass CheckMetricJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(public Healthcheck $healthcheck, public int $runId)\n    {\n        $this->onQueue(config()->string('healthchecks.queue'));\n    }\n\n    public function handle(CheckMetric $checkMetric, TeamService $teamService): void\n    {\n        $teamService->setTeamById($this->healthcheck->team_id);\n        $checkMetric->check($this->healthcheck, $this->runId);\n    }\n\n    public function uniqueId(): string\n    {\n        return 'metric-'.$this->healthcheck->id.'-'.$this->runId;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Jobs/CheckResultJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Healthchecks\\Actions\\CheckResult;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass CheckResultJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(public Healthcheck $healthcheck, public int $runId)\n    {\n        $this->onQueue(config()->string('healthchecks.queue'));\n    }\n\n    public function handle(CheckResult $result, TeamService $teamService): void\n    {\n        $teamService->setTeamById($this->healthcheck->team_id);\n        $result->check($this->healthcheck, $this->runId);\n    }\n\n    public function uniqueId(): string\n    {\n        return $this->healthcheck->id.'-'.$this->runId;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Livewire/Forms/HealthcheckForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Livewire\\Forms;\n\nuse Illuminate\\Validation\\Rule;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Form;\nuse Vigilant\\Core\\Validation\\CanEnableRule;\nuse Vigilant\\Healthchecks\\Enums\\Type;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass HealthcheckForm extends Form\n{\n    #[Locked]\n    public ?int $site_id;\n\n    public bool $enabled = true;\n\n    public string $domain = '';\n\n    public Type $type = Type::Endpoint;\n\n    public ?string $endpoint = null;\n\n    public int $interval = 60;\n\n    public function rules(): array\n    {\n        return [\n            'domain' => ['required', 'string', 'max:255', 'url'],\n            'type' => ['required', Rule::enum(Type::class)],\n            'endpoint' => ['nullable', 'string', 'max:255', 'required_if:type,endpoint'],\n            'interval' => ['required', 'integer', 'in:'.implode(',', array_keys(config('healthchecks.intervals')))],\n            'enabled' => ['boolean', new CanEnableRule(Healthcheck::class)],\n        ];\n    }\n\n    public function cleanDomain(): void\n    {\n        if (empty($this->domain)) {\n            return;\n        }\n\n        $parsed = parse_url($this->domain);\n        if ($parsed === false) {\n            return;\n        }\n\n        $scheme = $parsed['scheme'] ?? 'https';\n        $host = $parsed['host'] ?? $this->domain;\n        $port = isset($parsed['port']) ? \":{$parsed['port']}\" : '';\n\n        $this->domain = \"{$scheme}://{$host}{$port}\";\n    }\n\n    public function normalizeEndpoint(): void\n    {\n        if ($this->type !== Type::Endpoint && blank($this->endpoint)) {\n            $this->endpoint = null;\n        }\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Livewire/HealthcheckDashboard.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Livewire;\n\nuse Illuminate\\Contracts\\View\\View;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass HealthcheckDashboard extends Component\n{\n    #[Locked]\n    public int $healthcheckId;\n\n    public function mount(int $healthcheckId): void\n    {\n        $this->healthcheckId = $healthcheckId;\n    }\n\n    public function render(): View\n    {\n        $healthcheck = Healthcheck::query()->findOrFail($this->healthcheckId);\n\n        /** @var view-string $view */\n        $view = 'healthchecks::livewire.healthcheck-dashboard';\n\n        return view($view, [\n            'healthcheck' => $healthcheck,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Livewire/HealthcheckForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Livewire;\n\nuse Illuminate\\Contracts\\View\\View;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Healthchecks\\Enums\\Type;\nuse Vigilant\\Healthchecks\\Livewire\\Forms\\HealthcheckForm as HealthcheckFormObject;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass HealthcheckForm extends Component\n{\n    use DisplaysAlerts;\n\n    public HealthcheckFormObject $form;\n\n    #[Locked]\n    public Healthcheck $healthcheck;\n\n    public bool $inline = false;\n\n    public function mount(?Healthcheck $healthcheck, bool $inline = false): void\n    {\n        $this->inline = $inline;\n\n        if ($healthcheck !== null) {\n            $this->form->fill($healthcheck->except('type'));\n            $this->healthcheck = $healthcheck;\n            if ($healthcheck->exists) {\n                $this->authorize('update', $healthcheck);\n                $this->form->type = $healthcheck->type;\n            } else {\n                $this->authorize('create', $healthcheck);\n                /** @var array<int, string> $intervals */\n                $intervals = config('healthchecks.intervals', []);\n                /** @var int $defaultInterval */\n                $defaultInterval = collect($intervals)->keys()->first() ?? 60;\n                $this->form->interval = $defaultInterval;\n            }\n        }\n    }\n\n    #[On('save')]\n    public function save(): void\n    {\n        $this->form->cleanDomain();\n        $this->form->normalizeEndpoint();\n\n        $this->validate();\n\n        $isNew = ! $this->healthcheck->exists;\n\n        if ($this->healthcheck->exists) {\n            $this->authorize('update', $this->healthcheck);\n\n            $this->healthcheck->update($this->form->all());\n        } else {\n            $this->authorize('create', $this->healthcheck);\n\n            $this->healthcheck = Healthcheck::query()->create(\n                $this->form->all()\n            );\n        }\n\n        $this->alert(\n            __('Saved'),\n            __('Healthcheck was successfully :action',\n                ['action' => $this->healthcheck->wasRecentlyCreated ? 'created' : 'saved']),\n            AlertType::Success\n        );\n\n        if (! $this->inline) {\n            if ($isNew) {\n                if ($this->healthcheck->type === Type::Endpoint) {\n                    $this->redirectRoute('healthchecks.index');\n                } else {\n                    $this->redirectRoute('healthchecks.setup', ['healthcheck' => $this->healthcheck, 'new' => 1]);\n                }\n            } else {\n                $this->redirectRoute('healthchecks.index');\n            }\n        }\n    }\n\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'healthchecks::livewire.healthcheck-form';\n\n        return view($view, [\n            'updating' => $this->healthcheck->exists,\n            'inline' => $this->inline,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Livewire/HealthcheckSetup.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Livewire;\n\nuse Illuminate\\Contracts\\View\\View;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass HealthcheckSetup extends Component\n{\n    #[Locked]\n    public Healthcheck $healthcheck;\n\n    public bool $isNew = false;\n\n    public function mount(Healthcheck $healthcheck): void\n    {\n        $this->authorize('view', $healthcheck);\n        $this->healthcheck = $healthcheck;\n        $this->isNew = request()->query('new') === '1';\n    }\n\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'healthchecks::livewire.healthcheck-setup';\n\n        return view($view, [\n            'isNew' => $this->isNew,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Livewire/HealthcheckTokenEditor.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Livewire;\n\nuse Illuminate\\Contracts\\View\\View;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass HealthcheckTokenEditor extends Component\n{\n    use DisplaysAlerts;\n\n    #[Locked]\n    public Healthcheck $healthcheck;\n\n    public string $token = '';\n\n    public function mount(Healthcheck $healthcheck): void\n    {\n        $this->authorize('view', $healthcheck);\n        $this->healthcheck = $healthcheck;\n        $this->token = (string) $healthcheck->token;\n    }\n\n    protected function rules(): array\n    {\n        return [\n            'token' => ['required', 'string'],\n        ];\n    }\n\n    public function save(): void\n    {\n        if (! $this->healthcheck->type->generatesOwnToken()) {\n            return;\n        }\n\n        $this->authorize('update', $this->healthcheck);\n\n        $this->validate();\n\n        $this->healthcheck->update([\n            'token' => $this->token,\n        ]);\n\n        $this->healthcheck->refresh();\n        $this->token = (string) $this->healthcheck->token;\n\n        $this->alert(\n            __('Saved'),\n            __('Token updated successfully.'),\n            AlertType::Success\n        );\n    }\n\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'healthchecks::livewire.healthcheck-token-editor';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Livewire/Healthchecks.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Livewire;\n\nuse Illuminate\\Contracts\\View\\View;\nuse Livewire\\Component;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass Healthchecks extends Component\n{\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'healthchecks::livewire.healthchecks';\n        $hasHealthchecks = Healthcheck::query()->exists();\n\n        return view($view, [\n            'hasHealthchecks' => $hasHealthchecks,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Livewire/Tables/HealthcheckTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Enumerable;\nuse Illuminate\\Support\\Facades\\Gate;\nuse RamonRietdijk\\LivewireTables\\Actions\\Action;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse RamonRietdijk\\LivewireTables\\Filters\\SelectFilter;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\Enums\\Status;\nuse Vigilant\\Frontend\\Integrations\\Table\\StatusColumn;\nuse Vigilant\\Healthchecks\\Enums\\Status as HealthStatus;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass HealthcheckTable extends BaseTable\n{\n    protected string $model = Healthcheck::class;\n\n    protected array $pollingOptions = [\n        '' => 'None',\n        '30s' => 'Every 30 seconds',\n    ];\n\n    protected function columns(): array\n    {\n        return [\n            StatusColumn::make(__('Status'))\n                ->text(function (Healthcheck $healthcheck): string {\n                    if (! $healthcheck->enabled) {\n                        return __('Disabled');\n                    }\n\n                    if ($healthcheck->status === null) {\n                        return __('Unknown');\n                    }\n\n                    return match ($healthcheck->status) {\n                        HealthStatus::Healthy => __('Healthy'),\n                        HealthStatus::Unhealthy => __('Unhealthy'),\n                        default => __('Unknown'),\n                    };\n                })\n                ->status(function (Healthcheck $healthcheck): Status {\n                    if (! $healthcheck->enabled) {\n                        return Status::Danger;\n                    }\n\n                    if ($healthcheck->status === null) {\n                        return Status::Warning;\n                    }\n\n                    return match ($healthcheck->status) {\n                        HealthStatus::Healthy => Status::Success,\n                        HealthStatus::Unhealthy => Status::Danger,\n                        default => Status::Warning,\n                    };\n                }),\n\n            Column::make(__('Domain'), 'domain')\n                ->searchable()\n                ->sortable(),\n\n            Column::make(__('Last check'), 'last_check_at')\n                ->sortable(),\n        ];\n    }\n\n    protected function filters(): array\n    {\n        return [\n            SelectFilter::make(__('Site'), 'site_id')\n                ->options(\n                    Site::query()\n                        ->orderBy('url')\n                        ->pluck('url', 'id')\n                        ->toArray()\n                ),\n        ];\n    }\n\n    protected function actions(): array\n    {\n        return [\n            Action::make(__('Enable'), function (Enumerable $models): void {\n                foreach ($models as $model) {\n                    if (! Gate::allows('create', $model)) {\n                        break;\n                    }\n\n                    $model->update(['enabled' => true]);\n                }\n            }, 'enable'),\n\n            Action::make(__('Disable'), function (Enumerable $models): void {\n                $models->each(fn (Healthcheck $healthcheck) => $healthcheck->update(['enabled' => false]));\n            }, 'disable'),\n\n            Action::make(__('Delete'), function (Enumerable $models): void {\n                $models->each(fn (Healthcheck $healthcheck) => $healthcheck->delete());\n            }, 'delete'),\n        ];\n    }\n\n    public function link(Model $model): ?string\n    {\n        return route('healthchecks.view', ['healthcheck' => $model]);\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Livewire/Tables/ResultTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Livewire\\Attributes\\Locked;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\DateColumn;\nuse Vigilant\\Frontend\\Integrations\\Table\\Enums\\Status as TableStatus;\nuse Vigilant\\Frontend\\Integrations\\Table\\StatusColumn;\nuse Vigilant\\Healthchecks\\Enums\\Status;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\nuse Vigilant\\Healthchecks\\Models\\Result;\n\nclass ResultTable extends BaseTable\n{\n    protected string $model = Result::class;\n\n    #[Locked]\n    public int $healthcheckId = 0;\n\n    public string $sortColumn = 'created_at';\n\n    public string $sortDirection = 'desc';\n\n    public function mount(int $healthcheckId): void\n    {\n        $this->healthcheckId = $healthcheckId;\n        Healthcheck::query()->findOrFail($healthcheckId);\n    }\n\n    protected function columns(): array\n    {\n        return [\n            DateColumn::make(__('Last checked'), 'last_checked_at')\n                ->sortable(),\n\n            Column::make(__('Key'), 'key')\n                ->sortable(),\n\n            StatusColumn::make(__('Status'))\n                ->text(function (Result $result): string {\n                    return match ($result->status) {\n                        Status::Healthy => __('Healthy'),\n                        Status::Warning => __('Warning'),\n                        Status::Unhealthy => __('Unhealthy'),\n                    };\n                })\n                ->status(function (Result $result): TableStatus {\n                    return match ($result->status) {\n                        Status::Healthy => TableStatus::Success,\n                        Status::Warning => TableStatus::Warning,\n                        Status::Unhealthy => TableStatus::Danger,\n                    };\n                }),\n\n            Column::make(__('Message'), 'message'),\n\n            DateColumn::make(__('Last unhealthy'), 'last_unhealthy_at')\n                ->sortable(),\n        ];\n    }\n\n    protected function query(): Builder\n    {\n        return parent::query()\n            ->where('healthcheck_id', '=', $this->healthcheckId);\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Models/Healthcheck.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Healthchecks\\Enums\\Status;\nuse Vigilant\\Healthchecks\\Enums\\Type;\nuse Vigilant\\Healthchecks\\Observers\\HealthcheckObserver;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Observers\\TeamObserver;\n\n/**\n * @property int $id\n * @property ?int $site_id\n * @property int $team_id\n * @property bool $enabled\n * @property string $domain\n * @property Type $type\n * @property ?string $endpoint\n * @property string $token\n * @property ?Carbon $next_check_at\n * @property ?Carbon $last_check_at\n * @property int $interval\n * @property ?Status $status\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Site $site\n * @property ?Team $team\n * @property Collection<int, Result> $results\n * @property Collection<int, Metric> $metrics\n */\n#[ScopedBy([TeamScope::class])]\n#[ObservedBy([TeamObserver::class, HealthcheckObserver::class])]\nclass Healthcheck extends Model\n{\n    protected $guarded = [];\n\n    protected $casts = [\n        'enabled' => 'boolean',\n        'type' => Type::class,\n        'status' => Status::class,\n        'next_check_at' => 'datetime',\n        'last_check_at' => 'datetime',\n    ];\n\n    public function site(): BelongsTo\n    {\n        return $this->belongsTo(Site::class);\n    }\n\n    public function team(): BelongsTo\n    {\n        return $this->belongsTo(Team::class);\n    }\n\n    public function results(): HasMany\n    {\n        return $this->hasMany(Result::class);\n    }\n\n    public function metrics(): HasMany\n    {\n        return $this->hasMany(Metric::class);\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Models/Metric.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Prunable;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Concerns\\HasDataRetention;\n\n/**\n * @property int $id\n * @property int $healthcheck_id\n * @property ?int $run_id\n * @property string $key\n * @property float $value\n * @property ?string $unit\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Healthcheck $healthcheck\n */\nclass Metric extends Model\n{\n    use HasDataRetention;\n    use Prunable;\n\n    protected $table = 'healthcheck_metrics';\n\n    protected $guarded = [];\n\n    protected $casts = [\n        'value' => 'decimal:2',\n    ];\n\n    public function healthcheck(): BelongsTo\n    {\n        return $this->belongsTo(Healthcheck::class);\n    }\n\n    public function prunable(): Builder\n    {\n        return static::withoutGlobalScopes()->where('created_at', '<=', $this->retentionPeriod());\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Models/Result.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Prunable;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Concerns\\HasDataRetention;\nuse Vigilant\\Healthchecks\\Enums\\Status;\n\n/**\n * @property int $id\n * @property int $healthcheck_id\n * @property string $key\n * @property Status $status\n * @property ?string $message\n * @property ?array $data\n * @property ?Carbon $last_checked_at\n * @property ?Carbon $last_unhealthy_at\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Healthcheck $healthcheck\n */\nclass Result extends Model\n{\n    use HasDataRetention;\n    use Prunable;\n\n    protected $table = 'healthcheck_results';\n\n    protected $guarded = [];\n\n    protected $casts = [\n        'status' => Status::class,\n        'data' => 'array',\n        'last_checked_at' => 'datetime',\n        'last_unhealthy_at' => 'datetime',\n    ];\n\n    public function healthcheck(): BelongsTo\n    {\n        return $this->belongsTo(Healthcheck::class);\n    }\n\n    public function prunable(): Builder\n    {\n        return static::withoutGlobalScopes()->where('created_at', '<=', $this->retentionPeriod());\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/Conditions/CheckKeyCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications\\Conditions;\n\nuse Vigilant\\Healthchecks\\Models\\Result;\nuse Vigilant\\Healthchecks\\Notifications\\HealthCheckFailedNotification;\nuse Vigilant\\Notifications\\Conditions\\SelectCondition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass CheckKeyCondition extends SelectCondition\n{\n    public static string $name = 'Healthcheck';\n\n    public function options(): array\n    {\n        return Result::query()\n            ->whereNotNull('key')\n            ->distinct('key')\n            ->orderBy('key')\n            ->pluck('key', 'key')\n            ->toArray();\n    }\n\n    public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool\n    {\n        /** @var HealthCheckFailedNotification $notification */\n        return $notification->healthcheck->results()\n            ->where('key', '=', $value)\n            ->exists();\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/Conditions/DiskFullInCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications\\Conditions;\n\nuse Vigilant\\Healthchecks\\Notifications\\DiskUsageNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Enums\\ConditionType;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass DiskFullInCondition extends Condition\n{\n    public static string $name = 'Disk full in (hours)';\n\n    public ConditionType $type = ConditionType::Number;\n\n    public function operators(): array\n    {\n        return [\n            '<' => 'Less than',\n            '<=' => 'Less or equal than',\n            '>' => 'Greater than',\n            '>=' => 'Greater or equal than',\n        ];\n    }\n\n    public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool\n    {\n        /** @var DiskUsageNotification $notification */\n        $hoursUntilFull = $notification->hoursUntilFull;\n\n        $result = match ($operator) {\n            '>' => $hoursUntilFull > $value,\n            '>=' => $hoursUntilFull >= $value,\n            '<' => $hoursUntilFull < $value,\n            '<=' => $hoursUntilFull <= $value,\n            default => false,\n        };\n\n        return $result;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/Conditions/MetricIncreaseNewValueCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications\\Conditions;\n\nuse Vigilant\\Healthchecks\\Notifications\\MetricIncreasingNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Enums\\ConditionType;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass MetricIncreaseNewValueCondition extends Condition\n{\n    public static string $name = 'New value';\n\n    public ConditionType $type = ConditionType::Number;\n\n    public function operators(): array\n    {\n        return [\n            '<' => 'Less than',\n            '<=' => 'Less or equal than',\n            '>' => 'Greater than',\n            '>=' => 'Greater or equal than',\n        ];\n    }\n\n    public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool\n    {\n        /** @var MetricIncreasingNotification $notification */\n        $metricDatas = $notification->increasedMetrics;\n\n        if (empty($metricDatas)) {\n            return false;\n        }\n\n        if (! isset($metricDatas[0]) || ! is_array($metricDatas[0])) {\n            $metricDatas = [$metricDatas];\n        }\n\n        foreach ($metricDatas as $metricData) {\n            if (! is_array($metricData)) {\n                continue;\n            }\n\n            if ($operand !== null && ($metricData['key'] ?? null) !== $operand) {\n                continue;\n            }\n\n            $newValue = $metricData['new_value'] ?? $notification->metric->value ?? null;\n\n            if ($newValue === null) {\n                continue;\n            }\n\n            $result = match ($operator) {\n                '>' => $newValue > $value,\n                '>=' => $newValue >= $value,\n                '<' => $newValue < $value,\n                '<=' => $newValue <= $value,\n                default => false,\n            };\n\n            if ($result) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/Conditions/MetricIncreasePercentCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications\\Conditions;\n\nuse Vigilant\\Healthchecks\\Notifications\\MetricIncreasingNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Enums\\ConditionType;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass MetricIncreasePercentCondition extends Condition\n{\n    public static string $name = 'Increase percentage';\n\n    public ConditionType $type = ConditionType::Number;\n\n    public function operators(): array\n    {\n        return [\n            '<' => 'Less than',\n            '<=' => 'Less or equal than',\n            '>' => 'Greater than',\n            '>=' => 'Greater or equal than',\n        ];\n    }\n\n    public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool\n    {\n        /** @var MetricIncreasingNotification $notification */\n        $metricDatas = $notification->increasedMetrics;\n\n        if (empty($metricDatas)) {\n            return false;\n        }\n\n        if (! isset($metricDatas[0]) || ! is_array($metricDatas[0])) {\n            $metricDatas = [$metricDatas];\n        }\n\n        foreach ($metricDatas as $metricData) {\n            if (! is_array($metricData)) {\n                continue;\n            }\n\n            if ($operand !== null && ($metricData['key'] ?? null) !== $operand) {\n                continue;\n            }\n\n            $percentIncrease = $metricData['percent_increase'] ?? null;\n\n            if ($percentIncrease === null) {\n                continue;\n            }\n\n            $result = match ($operator) {\n                '>' => $percentIncrease > $value,\n                '>=' => $percentIncrease >= $value,\n                '<' => $percentIncrease < $value,\n                '<=' => $percentIncrease <= $value,\n                default => false,\n            };\n\n            if ($result) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/Conditions/MetricIncreaseTimeframeCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications\\Conditions;\n\nuse Vigilant\\Healthchecks\\Notifications\\MetricIncreasingNotification;\nuse Vigilant\\Notifications\\Conditions\\SelectCondition;\nuse Vigilant\\Notifications\\Enums\\ConditionType;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass MetricIncreaseTimeframeCondition extends SelectCondition\n{\n    public const INTERVALS = [2, 5, 10, 15, 30, 60];\n\n    public static string $name = 'Timeframe (minutes)';\n\n    public ConditionType $type = ConditionType::Select;\n\n    /** @return array<int, string> */\n    public function options(): array\n    {\n        $options = [];\n\n        foreach (self::INTERVALS as $minutes) {\n            $options[$minutes] = sprintf('%d minutes', $minutes);\n        }\n\n        return $options;\n    }\n\n    public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool\n    {\n        /** @var MetricIncreasingNotification $notification */\n        $metricDatas = $notification->increasedMetrics;\n\n        if (empty($metricDatas) || $value === null) {\n            return false;\n        }\n\n        if (! isset($metricDatas[0]) || ! is_array($metricDatas[0])) {\n            $metricDatas = [$metricDatas];\n        }\n\n        $threshold = (int) $value;\n\n        foreach ($metricDatas as $metricData) {\n            if (! is_array($metricData)) {\n                continue;\n            }\n\n            if ($operand !== null && ($metricData['key'] ?? null) !== $operand) {\n                continue;\n            }\n\n            $timeframeMinutes = $metricData['timeframe_minutes'] ?? null;\n\n            if ($timeframeMinutes === null) {\n                continue;\n            }\n\n            if ((int) $timeframeMinutes === $threshold) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/Conditions/MetricKeyCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications\\Conditions;\n\nuse Vigilant\\Healthchecks\\Models\\Metric;\nuse Vigilant\\Healthchecks\\Notifications\\MetricNotification;\nuse Vigilant\\Notifications\\Conditions\\SelectCondition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass MetricKeyCondition extends SelectCondition\n{\n    public static string $name = 'Metric key';\n\n    public function options(): array\n    {\n        return Metric::query()\n            ->whereNotNull('key')\n            ->distinct('key')\n            ->orderBy('key')\n            ->pluck('key', 'key')\n            ->toArray();\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'is',\n            '!=' => 'is not',\n        ];\n    }\n\n    public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool\n    {\n        /** @var MetricNotification $notification */\n\n        $key = $notification->metric->key;\n\n        return match ($operator) {\n            '=' => $key === $value,\n            '!=' => $key !== $value,\n            default => $key === $value,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/Conditions/MetricUnitCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications\\Conditions;\n\nuse Vigilant\\Healthchecks\\Models\\Metric;\nuse Vigilant\\Healthchecks\\Notifications\\MetricNotification;\nuse Vigilant\\Notifications\\Conditions\\SelectCondition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass MetricUnitCondition extends SelectCondition\n{\n    public static string $name = 'Metric unit';\n\n    public function options(): array\n    {\n        return Metric::query()\n            ->whereNotNull('unit')\n            ->distinct('unit')\n            ->orderBy('unit')\n            ->pluck('unit', 'unit')\n            ->toArray();\n    }\n\n    public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool\n    {\n        /** @var MetricNotification $notification */\n        return $notification->metric->unit === $value;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/Conditions/MetricValueCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications\\Conditions;\n\nuse Vigilant\\Healthchecks\\Notifications\\MetricNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Enums\\ConditionType;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass MetricValueCondition extends Condition\n{\n    public static string $name = 'Metric value';\n\n    public ConditionType $type = ConditionType::Number;\n\n    public function operators(): array\n    {\n        return [\n            '<' => 'Less than',\n            '<=' => 'Less or equal than',\n            '>' => 'Greater than',\n            '>=' => 'Greater or equal than',\n        ];\n    }\n\n    public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool\n    {\n        /** @var MetricNotification $notification */\n        $metric = $notification->metric->value;\n\n        return match ($operator) {\n            '>' => $metric > $value,\n            '>=' => $metric >= $value,\n            '<' => $metric < $value,\n            '<=' => $metric <= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/Conditions/StatusCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications\\Conditions;\n\nuse Vigilant\\Healthchecks\\Enums\\Status;\nuse Vigilant\\Healthchecks\\Notifications\\HealthCheckFailedNotification;\nuse Vigilant\\Notifications\\Conditions\\SelectCondition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass StatusCondition extends SelectCondition\n{\n    public static string $name = 'Status';\n\n    public function options(): array\n    {\n        return [\n            Status::Unhealthy->value => 'Unhealthy',\n            Status::Warning->value => 'Warning',\n        ];\n    }\n\n    public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool\n    {\n        /** @var HealthCheckFailedNotification $notification */\n        $status = Status::tryFrom($value);\n        if ($status === null) {\n            return false;\n        }\n\n        return $notification->healthcheck->results()\n            ->where('status', '=', $status)\n            ->exists();\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/DiskUsageNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications;\n\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\DiskFullInCondition;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass DiskUsageNotification extends Notification implements HasSite\n{\n    public static string $name = 'Disk usage';\n\n    public Level $level = Level::Critical;\n\n    public static ?int $defaultCooldown = 60;\n\n    public static array $defaultConditions = [\n        'type' => 'group',\n        'operator' => 'any',\n        'children' => [\n            [\n                'type' => 'condition',\n                'condition' => DiskFullInCondition::class,\n                'operator' => '<=',\n                'value' => 24,\n            ],\n        ],\n    ];\n\n    public function __construct(\n        public Healthcheck $healthcheck,\n        public float $currentUsage,\n        public float $velocity,\n        public float $hoursUntilFull,\n        public ?Carbon $estimatedFullAt = null\n    ) {}\n\n    public function title(): string\n    {\n        $domain = $this->healthcheck->domain;\n\n        return __('Disk usage critical for :domain', ['domain' => $domain]);\n    }\n\n    public function description(): string\n    {\n        $hours = round($this->hoursUntilFull, 1);\n        $velocityPerHour = round($this->velocity, 2);\n\n        $message = __('Current disk usage: :usage%', ['usage' => round($this->currentUsage, 2)]).PHP_EOL;\n        $message .= __('Growth rate: :rate% per hour', ['rate' => $velocityPerHour]).PHP_EOL;\n        $message .= __('Estimated to reach 100% in: :hours hours', ['hours' => $hours]).PHP_EOL;\n\n        if ($this->estimatedFullAt) {\n            $message .= __('Estimated full at: :time', ['time' => teamTimezone($this->estimatedFullAt)->toDateTimeString()]);\n        }\n\n        return $message;\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when disk usage is projected to reach 100% within a specified timeframe.');\n    }\n\n    public function uniqueId(): string\n    {\n        return 'disk-usage-'.$this->healthcheck->id;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->healthcheck->site;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/HealthCheckFailedNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications;\n\nuse Vigilant\\Healthchecks\\Enums\\Status;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\nuse Vigilant\\Healthchecks\\Models\\Result;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\StatusCondition;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass HealthCheckFailedNotification extends Notification implements HasSite\n{\n    public static string $name = 'Healthcheck failed';\n\n    public Level $level = Level::Critical;\n\n    public static ?int $defaultCooldown = 60;\n\n    public static array $defaultConditions = [\n        'type' => 'group',\n        'operator' => 'any',\n        'children' => [\n            [\n                'type' => 'condition',\n                'condition' => StatusCondition::class,\n                'value' => Status::Unhealthy->value,\n            ],\n        ],\n    ];\n\n    public function __construct(\n        public Healthcheck $healthcheck,\n        public int $runId\n    ) {}\n\n    public function title(): string\n    {\n        return __('Healthcheck failed for :domain', ['domain' => $this->healthcheck->domain]);\n    }\n\n    public function description(): string\n    {\n        /** @var \\Illuminate\\Database\\Eloquent\\Collection<int, Result> $results */\n        $results = $this->healthcheck->results()\n            ->where('status', '!=', Status::Healthy)\n            ->get();\n\n        $failedChecks = $results->map(function (Result $result): string {\n            if ($result->message === null) {\n                return $result->key;\n            }\n\n            return $result->key.': '.$result->message;\n        })->implode(PHP_EOL);\n\n        return __('Healthchecks have failed:').PHP_EOL.$failedChecks;\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when a healthcheck fails.');\n    }\n\n    public function uniqueId(): string\n    {\n        /** @var \\Illuminate\\Database\\Eloquent\\Collection<int, Result> $results */\n        $results = $this->healthcheck->results()\n            ->where('status', '!=', Status::Healthy)\n            ->orderBy('key')\n            ->get();\n\n        $keys = $results\n            ->pluck('key')\n            ->implode('-');\n\n        return $this->healthcheck->id.'-'.$keys;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->healthcheck->site;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/MetricIncreasingNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications;\n\nuse Vigilant\\Healthchecks\\Models\\Metric;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricIncreaseNewValueCondition;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricIncreasePercentCondition;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricIncreaseTimeframeCondition;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass MetricIncreasingNotification extends Notification implements HasSite\n{\n    public static string $name = 'Metric increasing';\n\n    public Level $level = Level::Warning;\n\n    public static ?int $defaultCooldown = 60;\n\n    public static array $defaultConditions = [\n        'type' => 'group',\n        'operator' => 'all',\n        'children' => [\n            [\n                'type' => 'condition',\n                'condition' => MetricIncreasePercentCondition::class,\n                'operator' => '>=',\n                'value' => 25,\n            ],\n            [\n                'type' => 'condition',\n                'condition' => MetricIncreaseNewValueCondition::class,\n                'operator' => '>=',\n                'value' => 20,\n            ],\n            [\n                'type' => 'condition',\n                'condition' => MetricIncreaseTimeframeCondition::class,\n                'operator' => '=',\n                'value' => 10,\n            ],\n        ],\n    ];\n\n    public function __construct(\n        public Metric $metric,\n        public array $increasedMetrics = []\n    ) {}\n\n    public function title(): string\n    {\n        $domain = $this->metric->healthcheck->domain ?? '?';\n\n        return __('Metric increasing for :domain', ['domain' => $domain]);\n    }\n\n    public function description(): string\n    {\n        $key = $this->metric->key;\n        $value = $this->metric->value;\n        $unit = $this->metric->unit;\n\n        if (! empty($this->increasedMetrics)) {\n            $metricData = $this->increasedMetrics;\n\n            if (isset($metricData[0]) && is_array($metricData[0])) {\n                $sorted = $metricData;\n                usort($sorted, static function (array $left, array $right): int {\n                    return ($left['timeframe_minutes'] ?? PHP_INT_MAX) <=> ($right['timeframe_minutes'] ?? PHP_INT_MAX);\n                });\n                $metricData = $sorted[0];\n            }\n\n            if (! empty($metricData)) {\n                $percentIncrease = round($metricData['percent_increase'] ?? 0, 1);\n                $timeframeMinutes = $metricData['timeframe_minutes'] ?? 0;\n                $oldValue = $metricData['old_value'] ?? 0;\n\n                return __('The metric \":key\" has increased by :percent% (from :old_value:unit to :new_value:unit) over the past :timeframe minutes.', [\n                    'key' => $key,\n                    'percent' => $percentIncrease,\n                    'old_value' => $oldValue,\n                    'new_value' => $value,\n                    'unit' => $unit,\n                    'timeframe' => $timeframeMinutes,\n                ]);\n            }\n        }\n\n        return __('The metric \":key\" has increased to :value:unit.', [\n            'key' => $key,\n            'value' => $value,\n            'unit' => $unit,\n        ]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when a metric increases by a specified percentage within a timeframe.');\n    }\n\n    public function uniqueId(): string\n    {\n        return 'metric-increasing-'.$this->metric->key;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->metric->healthcheck?->site;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/MetricNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications;\n\nuse Vigilant\\Healthchecks\\Models\\Metric;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricUnitCondition;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricValueCondition;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass MetricNotification extends Notification implements HasSite\n{\n    public static string $name = 'Metric threshold exceeded';\n\n    public Level $level = Level::Warning;\n\n    public static ?int $defaultCooldown = 60;\n\n    public static array $defaultConditions = [\n        'type' => 'group',\n        'operator' => 'all',\n        'children' => [\n            [\n                'type' => 'condition',\n                'condition' => MetricUnitCondition::class,\n                'value' => '%',\n            ],\n            [\n                'type' => 'condition',\n                'condition' => MetricValueCondition::class,\n                'operator' => '>',\n                'value' => 80,\n            ],\n        ],\n    ];\n\n    public function __construct(\n        public Metric $metric\n    ) {}\n\n    public function title(): string\n    {\n        $domain = $this->metric->healthcheck->domain ?? '?';\n\n        return __('Metric threshold exceeded for :domain', ['domain' => $domain]);\n    }\n\n    public function description(): string\n    {\n        $key = $this->metric->key;\n        $value = $this->metric->value;\n        $unit = $this->metric->unit;\n\n        return __('The metric \":key\" has exceeded its configured threshold with a value of :value:unit.', [\n            'key' => $key,\n            'value' => $value,\n            'unit' => $unit,\n        ]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when a healthcheck metric exceeds configured thresholds.');\n    }\n\n    public function uniqueId(): string\n    {\n        return $this->metric->key;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->metric->healthcheck?->site;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Notifications/MetricSpikeNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Notifications;\n\nuse Vigilant\\Healthchecks\\Models\\Metric;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricIncreasePercentCondition;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricIncreaseTimeframeCondition;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass MetricSpikeNotification extends Notification implements HasSite\n{\n    public static string $name = 'Metric spike detected';\n\n    public Level $level = Level::Critical;\n\n    public static ?int $defaultCooldown = 15;\n\n    public static array $defaultConditions = [\n        'type' => 'group',\n        'operator' => 'all',\n        'children' => [\n            [\n                'type' => 'condition',\n                'condition' => MetricIncreasePercentCondition::class,\n                'operator' => '>=',\n                'value' => 40,\n            ],\n            [\n                'type' => 'condition',\n                'condition' => MetricIncreaseTimeframeCondition::class,\n                'operator' => '<=',\n                'value' => 5,\n            ],\n        ],\n    ];\n\n    public function __construct(\n        public Metric $metric,\n        public array $spikeMetrics = []\n    ) {}\n\n    public function title(): string\n    {\n        $domain = $this->metric->healthcheck->domain ?? '?';\n\n        return __('Metric spike detected for :domain', ['domain' => $domain]);\n    }\n\n    public function description(): string\n    {\n        $key = $this->metric->key;\n        $value = $this->metric->value;\n        $unit = $this->metric->unit;\n\n        if (! empty($this->spikeMetrics)) {\n            $percentIncrease = round($this->spikeMetrics['percent_increase'] ?? 0, 1);\n            $timeframeMinutes = $this->spikeMetrics['timeframe_minutes'] ?? 0;\n            $oldValue = $this->spikeMetrics['old_value'] ?? 0;\n\n            return __('The metric \":key\" suddenly spiked by :percent% (from :old_value:unit to :new_value:unit) within :timeframe minutes.', [\n                'key' => $key,\n                'percent' => $percentIncrease,\n                'old_value' => $oldValue,\n                'new_value' => $value,\n                'unit' => $unit,\n                'timeframe' => $timeframeMinutes,\n            ]);\n        }\n\n        return __('The metric \":key\" suddenly spiked to :value:unit.', [\n            'key' => $key,\n            'value' => $value,\n            'unit' => $unit,\n        ]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when a metric suddenly spikes by a large percentage in a short timeframe.');\n    }\n\n    public function uniqueId(): string\n    {\n        return 'metric-spike-'.$this->metric->key;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->metric->healthcheck?->site;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/Observers/HealthcheckObserver.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Observers;\n\nuse Illuminate\\Support\\Str;\nuse Vigilant\\Healthchecks\\Jobs\\CheckHealthcheckJob;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\n\nclass HealthcheckObserver\n{\n    public function creating(Healthcheck $healthcheck): void\n    {\n        if (empty($healthcheck->token)) {\n            $healthcheck->token = Str::random(32);\n        }\n    }\n\n    public function created(Healthcheck $healthcheck): void\n    {\n        CheckHealthcheckJob::dispatch($healthcheck);\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks;\n\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\nuse Livewire\\Livewire;\nuse Vigilant\\Core\\Facades\\Navigation;\nuse Vigilant\\Core\\Policies\\AllowAllPolicy;\nuse Vigilant\\Healthchecks\\Commands\\AggregateMetricsCommand;\nuse Vigilant\\Healthchecks\\Commands\\CheckHealthcheckCommand;\nuse Vigilant\\Healthchecks\\Commands\\ScheduleHealthchecksCommand;\nuse Vigilant\\Healthchecks\\Http\\Livewire\\Charts\\MetricChart;\nuse Vigilant\\Healthchecks\\Livewire\\HealthcheckDashboard;\nuse Vigilant\\Healthchecks\\Livewire\\HealthcheckForm;\nuse Vigilant\\Healthchecks\\Livewire\\HealthcheckTokenEditor;\nuse Vigilant\\Healthchecks\\Livewire\\Healthchecks;\nuse Vigilant\\Healthchecks\\Livewire\\Tables\\HealthcheckTable;\nuse Vigilant\\Healthchecks\\Livewire\\Tables\\ResultTable;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\CheckKeyCondition;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\DiskFullInCondition;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricIncreaseNewValueCondition;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricIncreasePercentCondition;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricIncreaseTimeframeCondition;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricKeyCondition;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricUnitCondition;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricValueCondition;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\StatusCondition;\nuse Vigilant\\Healthchecks\\Notifications\\DiskUsageNotification;\nuse Vigilant\\Healthchecks\\Notifications\\HealthCheckFailedNotification;\nuse Vigilant\\Healthchecks\\Notifications\\MetricIncreasingNotification;\nuse Vigilant\\Healthchecks\\Notifications\\MetricNotification;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Sites\\Conditions\\SiteCondition;\nuse Vigilant\\Users\\Models\\User;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function register(): void\n    {\n        $this\n            ->registerConfig();\n    }\n\n    protected function registerConfig(): static\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/healthchecks.php', 'healthchecks');\n\n        return $this;\n    }\n\n    public function boot(): void\n    {\n        $this\n            ->bootConfig()\n            ->bootMigrations()\n            ->bootCommands()\n            ->bootViews()\n            ->bootLivewire()\n            ->bootRoutes()\n            ->bootNavigation()\n            ->bootNotifications()\n            ->bootGates()\n            ->bootPolicies();\n    }\n\n    protected function bootConfig(): static\n    {\n        $this->publishes([\n            __DIR__.'/../config/healthchecks.php' => config_path('healthchecks.php'),\n        ], 'config');\n\n        return $this;\n    }\n\n    protected function bootMigrations(): static\n    {\n        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');\n\n        return $this;\n    }\n\n    protected function bootCommands(): static\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n                AggregateMetricsCommand::class,\n                CheckHealthcheckCommand::class,\n                ScheduleHealthchecksCommand::class,\n            ]);\n        }\n\n        return $this;\n    }\n\n    protected function bootViews(): static\n    {\n        $this->loadViewsFrom(__DIR__.'/../resources/views', 'healthchecks');\n\n        return $this;\n    }\n\n    protected function bootLivewire(): static\n    {\n        Livewire::component('healthchecks', Healthchecks::class);\n        Livewire::component('healthcheck-form', HealthcheckForm::class);\n        Livewire::component('healthcheck-table', HealthcheckTable::class);\n        Livewire::component('healthcheck-result-table', ResultTable::class);\n        Livewire::component('healthcheck-metric-chart', MetricChart::class);\n        Livewire::component('healthcheck-dashboard', HealthcheckDashboard::class);\n        Livewire::component('healthcheck-token-editor', HealthcheckTokenEditor::class);\n\n        return $this;\n    }\n\n    protected function bootRoutes(): static\n    {\n        if (! $this->app->routesAreCached()) {\n            Route::middleware(['web', 'auth'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/web.php'));\n\n            Route::prefix('api')\n                ->middleware(['api'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/api.php'));\n        }\n\n        return $this;\n    }\n\n    protected function bootNavigation(): static\n    {\n        Navigation::path(__DIR__.'/../resources/navigation.php');\n\n        return $this;\n    }\n\n    protected function bootNotifications(): static\n    {\n        NotificationRegistry::registerNotification([\n            HealthCheckFailedNotification::class,\n            MetricNotification::class,\n            MetricIncreasingNotification::class,\n            DiskUsageNotification::class,\n        ]);\n\n        NotificationRegistry::registerCondition(HealthCheckFailedNotification::class, [\n            StatusCondition::class,\n            CheckKeyCondition::class,\n            SiteCondition::class,\n        ]);\n\n        NotificationRegistry::registerCondition(MetricNotification::class, [\n            MetricKeyCondition::class,\n            MetricValueCondition::class,\n            MetricUnitCondition::class,\n            SiteCondition::class,\n        ]);\n\n        NotificationRegistry::registerCondition(MetricIncreasingNotification::class, [\n            MetricKeyCondition::class,\n            MetricIncreasePercentCondition::class,\n            MetricIncreaseNewValueCondition::class,\n            MetricIncreaseTimeframeCondition::class,\n            SiteCondition::class,\n        ]);\n\n        NotificationRegistry::registerCondition(DiskUsageNotification::class, [\n            DiskFullInCondition::class,\n            SiteCondition::class,\n        ]);\n\n        return $this;\n    }\n\n    protected function bootGates(): static\n    {\n        Gate::define('use-healthchecks', function (User $user) {\n            return ce();\n        });\n\n        return $this;\n    }\n\n    protected function bootPolicies(): static\n    {\n        if (ce()) {\n            Gate::policy(Healthcheck::class, AllowAllPolicy::class);\n        }\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/testbench.yaml",
    "content": "providers:\n  - Vigilant\\Healthchecks\\ServiceProvider\n"
  },
  {
    "path": "packages/healthchecks/tests/Actions/AggregateMetricsTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Tests\\Actions;\n\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Healthchecks\\Actions\\AggregateMetrics;\nuse Vigilant\\Healthchecks\\Enums\\Type;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\nuse Vigilant\\Healthchecks\\Models\\Metric;\nuse Vigilant\\Healthchecks\\Tests\\TestCase;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Models\\User;\n\nclass AggregateMetricsTest extends TestCase\n{\n    public function test_it_aggregates_metrics_older_than_the_last_hour(): void\n    {\n        Carbon::setTestNow(Carbon::parse('2025-01-01 12:00:00'));\n\n        $user = User::factory()->create();\n        $team = Team::factory()->create(['user_id' => $user->id]);\n\n        $healthcheck = Healthcheck::query()->create([\n            'team_id' => $team->id,\n            'site_id' => null,\n            'enabled' => true,\n            'domain' => 'example.com',\n            'type' => Type::Endpoint->value,\n            'token' => 'token',\n            'interval' => 5,\n        ]);\n\n        $first = Metric::query()->create([\n            'healthcheck_id' => $healthcheck->id,\n            'key' => 'cpu',\n            'value' => 10,\n            'unit' => '%',\n            'created_at' => Carbon::parse('2025-01-01 10:05:00'),\n            'updated_at' => Carbon::parse('2025-01-01 10:05:00'),\n        ]);\n\n        $second = Metric::query()->create([\n            'healthcheck_id' => $healthcheck->id,\n            'key' => 'cpu',\n            'value' => 20,\n            'unit' => '%',\n            'created_at' => Carbon::parse('2025-01-01 10:15:00'),\n            'updated_at' => Carbon::parse('2025-01-01 10:15:00'),\n        ]);\n\n        $third = Metric::query()->create([\n            'healthcheck_id' => $healthcheck->id,\n            'key' => 'cpu',\n            'value' => 30,\n            'unit' => '%',\n            'created_at' => Carbon::parse('2025-01-01 10:45:00'),\n            'updated_at' => Carbon::parse('2025-01-01 10:45:00'),\n        ]);\n\n        $recentOne = Metric::query()->create([\n            'healthcheck_id' => $healthcheck->id,\n            'key' => 'cpu',\n            'value' => 60,\n            'unit' => '%',\n            'created_at' => Carbon::parse('2025-01-01 11:05:00'),\n            'updated_at' => Carbon::parse('2025-01-01 11:05:00'),\n        ]);\n\n        $recentTwo = Metric::query()->create([\n            'healthcheck_id' => $healthcheck->id,\n            'key' => 'cpu',\n            'value' => 70,\n            'unit' => '%',\n            'created_at' => Carbon::parse('2025-01-01 11:20:00'),\n            'updated_at' => Carbon::parse('2025-01-01 11:20:00'),\n        ]);\n\n        app(AggregateMetrics::class)->handle();\n\n        $this->assertDatabaseCount('healthcheck_metrics', 3);\n\n        $firstRefreshed = $first->fresh();\n\n        $this->assertInstanceOf(Metric::class, $firstRefreshed);\n        $this->assertSame('20.00', $firstRefreshed->value);\n        $this->assertDatabaseMissing('healthcheck_metrics', ['id' => $second->id]);\n        $this->assertDatabaseMissing('healthcheck_metrics', ['id' => $third->id]);\n        $this->assertDatabaseHas('healthcheck_metrics', ['id' => $recentOne->id]);\n        $this->assertDatabaseHas('healthcheck_metrics', ['id' => $recentTwo->id]);\n\n        $this->assertSame(2, Metric::query()\n            ->where('created_at', '>=', Carbon::parse('2025-01-01 11:00:00'))\n            ->count());\n\n        Carbon::setTestNow();\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/tests/Actions/CheckMetricTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Tests\\Actions;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Facades\\Bus;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Healthchecks\\Actions\\CheckMetric;\nuse Vigilant\\Healthchecks\\Enums\\Type;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\nuse Vigilant\\Healthchecks\\Models\\Metric;\nuse Vigilant\\Healthchecks\\Notifications\\Conditions\\MetricIncreaseTimeframeCondition;\nuse Vigilant\\Healthchecks\\Notifications\\DiskUsageNotification;\nuse Vigilant\\Healthchecks\\Notifications\\MetricIncreasingNotification;\nuse Vigilant\\Healthchecks\\Notifications\\MetricSpikeNotification;\nuse Vigilant\\Healthchecks\\Tests\\TestCase;\n\nclass CheckMetricTest extends TestCase\n{\n    #[Test]\n    public function it_does_nothing_when_no_metrics_exist(): void\n    {\n        MetricIncreasingNotification::fake();\n        MetricSpikeNotification::fake();\n\n        Bus::fake();\n\n        $healthcheck = Healthcheck::query()->create([\n            'domain' => 'example.com',\n            'type' => Type::Laravel,\n            'interval' => 5,\n            'token' => 'test-token',\n        ]);\n\n        /** @var CheckMetric $action */\n        $action = app(CheckMetric::class);\n        $action->check($healthcheck, 1);\n\n        $this->assertFalse(MetricIncreasingNotification::wasDispatched());\n        $this->assertFalse(MetricSpikeNotification::wasDispatched());\n    }\n\n    #[Test]\n    public function it_detects_metric_spike(): void\n    {\n        MetricSpikeNotification::fake();\n\n        $healthcheck = Healthcheck::query()->create([\n            'domain' => 'example.com',\n            'type' => Type::Laravel,\n            'interval' => 5,\n            'token' => 'test-token',\n        ]);\n\n        $now = Carbon::now();\n\n        // Create four stable metrics and a sudden spike on the fifth run\n        foreach ([100, 105, 110, 115] as $index => $value) {\n            Metric::query()->create([\n                'healthcheck_id' => $healthcheck->id,\n                'run_id' => $index + 1,\n                'key' => 'memory_usage',\n                'value' => $value,\n                'unit' => 'MB',\n                'created_at' => $now->copy()->subMinutes(4 - $index),\n            ]);\n        }\n\n        Metric::query()->create([\n            'healthcheck_id' => $healthcheck->id,\n            'run_id' => 5,\n            'key' => 'memory_usage',\n            'value' => 200,\n            'unit' => 'MB',\n            'created_at' => $now,\n        ]);\n\n        /** @var CheckMetric $action */\n        $action = app(CheckMetric::class);\n        $action->check($healthcheck, 5);\n\n        $this->assertTrue(MetricSpikeNotification::wasDispatched(function ($notification): bool {\n            if (! $notification instanceof MetricSpikeNotification) {\n                return true;\n            }\n\n            return $notification->spikeMetrics['key'] === 'memory_usage' &&\n                $notification->spikeMetrics['old_value'] == 100 &&\n                $notification->spikeMetrics['new_value'] == 200 &&\n                $notification->spikeMetrics['percent_increase'] == 100 &&\n                $notification->spikeMetrics['sample_size'] == 5 &&\n                $notification->spikeMetrics['detection_type'] === 'sudden_spike';\n        }));\n    }\n\n    #[Test]\n    public function it_detects_long_term_metric_increase(): void\n    {\n        MetricIncreasingNotification::fake();\n\n        $healthcheck = Healthcheck::query()->create([\n            'domain' => 'example.com',\n            'type' => Type::Laravel,\n            'interval' => 5,\n            'token' => 'test-token',\n        ]);\n\n        $now = Carbon::now();\n\n        // Create 7 historical metrics covering the last 60 minutes\n        for ($i = 0; $i < 7; $i++) {\n            Metric::query()->create([\n                'healthcheck_id' => $healthcheck->id,\n                'run_id' => $i + 1,\n                'key' => 'memory_usage',\n                'value' => 100 + ($i * 15),\n                'unit' => 'MB',\n                'created_at' => $now->copy()->subMinutes(60 - ($i * 10)),\n            ]);\n        }\n\n        /** @var CheckMetric $action */\n        $action = app(CheckMetric::class);\n        $action->check($healthcheck, 7);\n\n        $matched = false;\n\n        MetricIncreasingNotification::wasDispatched(function ($notification) use (&$matched): bool {\n            if (! $notification instanceof MetricIncreasingNotification) {\n                return true;\n            }\n\n            $entries = $notification->increasedMetrics;\n\n            if (! isset($entries[0]) || ! is_array($entries[0])) {\n                return true;\n            }\n\n            $match = collect($entries)->first(function (array $entry): bool {\n                return ($entry['key'] ?? null) === 'memory_usage'\n                    && ($entry['old_value'] ?? null) == 100\n                    && ($entry['new_value'] ?? null) == 190\n                    && round($entry['percent_increase'] ?? 0, 0) == 90\n                    && ($entry['timeframe_minutes'] ?? null) === 60;\n            });\n\n            if ($match !== null) {\n                $matched = true;\n            }\n\n            return true;\n        });\n\n        $this->assertTrue($matched);\n    }\n\n    #[Test]\n    public function it_only_checks_configured_timeframes(): void\n    {\n        MetricIncreasingNotification::fake();\n        MetricSpikeNotification::fake();\n\n        $healthcheck = Healthcheck::query()->create([\n            'domain' => 'example.com',\n            'type' => Type::Laravel,\n            'interval' => 5,\n            'token' => 'interval-test-token',\n        ]);\n\n        $now = Carbon::now();\n        $runId = 0;\n\n        $dataPoints = [\n            60 => 10,\n            30 => 15,\n            15 => 18,\n            10 => 20,\n            5 => 22,\n            2 => 23,\n            0 => 24,\n        ];\n\n        foreach ($dataPoints as $minutesAgo => $value) {\n            $runId++;\n\n            Metric::query()->create([\n                'healthcheck_id' => $healthcheck->id,\n                'run_id' => $runId,\n                'key' => 'cpu_load',\n                'value' => $value,\n                'unit' => '%',\n                'created_at' => $now->copy()->subMinutes($minutesAgo),\n            ]);\n        }\n\n        /** @var CheckMetric $action */\n        $action = app(CheckMetric::class);\n        $action->check($healthcheck, $runId);\n\n        $matched = false;\n\n        MetricIncreasingNotification::wasDispatched(function ($notification) use (&$matched): bool {\n            if (! $notification instanceof MetricIncreasingNotification) {\n                return true;\n            }\n\n            $entries = array_filter(\n                $notification->increasedMetrics,\n                static fn ($entry) => is_array($entry)\n            );\n\n            if ($entries === []) {\n                return true;\n            }\n\n            $timeframes = array_map(\n                static fn (array $entry) => $entry['timeframe_minutes'] ?? null,\n                $entries\n            );\n\n            $timeframes = array_filter($timeframes, static fn ($value) => $value !== null);\n            sort($timeframes);\n\n            if ($timeframes === MetricIncreaseTimeframeCondition::INTERVALS) {\n                $matched = true;\n            }\n\n            return true;\n        });\n\n        $this->assertTrue($matched);\n    }\n\n    #[Test]\n    public function it_notifies_when_disk_usage_is_increasing(): void\n    {\n        DiskUsageNotification::fake();\n\n        $healthcheck = Healthcheck::query()->create([\n            'domain' => 'example.com',\n            'type' => Type::Laravel,\n            'interval' => 5,\n            'token' => 'disk-test-token',\n        ]);\n\n        $now = Carbon::now();\n\n        Metric::query()->create([\n            'healthcheck_id' => $healthcheck->id,\n            'run_id' => 1,\n            'key' => 'disk_usage',\n            'value' => 70,\n            'unit' => '%',\n            'created_at' => $now->copy()->subHours(5),\n        ]);\n\n        Metric::query()->create([\n            'healthcheck_id' => $healthcheck->id,\n            'run_id' => 2,\n            'key' => 'disk_usage',\n            'value' => 90,\n            'unit' => '%',\n            'created_at' => $now,\n        ]);\n\n        /** @var CheckMetric $action */\n        $action = app(CheckMetric::class);\n        $action->check($healthcheck, 2);\n\n        $this->assertTrue(DiskUsageNotification::wasDispatched(function ($notification) use ($healthcheck): bool {\n            if (! $notification instanceof DiskUsageNotification) {\n                return true;\n            }\n\n            return $notification->healthcheck->is($healthcheck) &&\n                $notification->currentUsage === 90.0 &&\n                $notification->velocity === 4.0 &&\n                $notification->hoursUntilFull === 2.5;\n        }));\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/tests/Actions/CheckResultTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Tests\\Actions;\n\nuse Illuminate\\Support\\Facades\\Bus;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Healthchecks\\Actions\\CheckResult;\nuse Vigilant\\Healthchecks\\Enums\\Status;\nuse Vigilant\\Healthchecks\\Enums\\Type;\nuse Vigilant\\Healthchecks\\Jobs\\CheckMetricJob;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\nuse Vigilant\\Healthchecks\\Models\\Result;\nuse Vigilant\\Healthchecks\\Notifications\\HealthCheckFailedNotification;\nuse Vigilant\\Healthchecks\\Tests\\TestCase;\n\nclass CheckResultTest extends TestCase\n{\n    #[Test]\n    public function it_notifies_when_unhealthy(): void\n    {\n        Bus::fake();\n        $this->fakeNotification(HealthCheckFailedNotification::class);\n\n        $healthcheck = Healthcheck::query()->create([\n            'domain' => 'example.com',\n            'type' => Type::Laravel,\n            'interval' => 5,\n            'token' => 'result-test-token',\n        ]);\n\n        Result::query()->create([\n            'healthcheck_id' => $healthcheck->id,\n            'key' => 'uptime',\n            'status' => Status::Unhealthy,\n            'message' => 'Service unavailable',\n        ]);\n\n        /** @var CheckResult $action */\n        $action = app(CheckResult::class);\n        $action->check($healthcheck, 42);\n\n        $this->assertNotificationDispatched(\n            HealthCheckFailedNotification::class,\n            function (HealthCheckFailedNotification $notification) use ($healthcheck): bool {\n                return $notification->healthcheck->is($healthcheck)\n                    && $notification->runId === 42;\n            }\n        );\n\n        $refreshedHealthcheck = $healthcheck->fresh();\n        $this->assertInstanceOf(Healthcheck::class, $refreshedHealthcheck);\n        $this->assertSame(Status::Unhealthy, $refreshedHealthcheck->status);\n\n        Bus::assertDispatched(CheckMetricJob::class, function (CheckMetricJob $job) use ($healthcheck) {\n            return $job->healthcheck->is($healthcheck)\n                && $job->runId === 42;\n        });\n    }\n\n    #[Test]\n    public function it_sets_status_to_warning_when_results_have_warnings(): void\n    {\n        Bus::fake();\n        $this->fakeNotification(HealthCheckFailedNotification::class);\n\n        $healthcheck = Healthcheck::query()->create([\n            'domain' => 'example.com',\n            'type' => Type::Laravel,\n            'interval' => 5,\n            'token' => 'result-warning-token',\n        ]);\n\n        Result::query()->create([\n            'healthcheck_id' => $healthcheck->id,\n            'key' => 'uptime',\n            'status' => Status::Warning,\n            'message' => 'Slow response',\n        ]);\n\n        /** @var CheckResult $action */\n        $action = app(CheckResult::class);\n        $action->check($healthcheck, 7);\n\n        $this->assertNotificationDispatched(\n            HealthCheckFailedNotification::class,\n            function (HealthCheckFailedNotification $notification) use ($healthcheck): bool {\n                return $notification->healthcheck->is($healthcheck)\n                    && $notification->runId === 7;\n            }\n        );\n\n        $refreshedHealthcheck = $healthcheck->fresh();\n        $this->assertInstanceOf(Healthcheck::class, $refreshedHealthcheck);\n        $this->assertSame(Status::Warning, $refreshedHealthcheck->status);\n\n        Bus::assertDispatched(CheckMetricJob::class, function (CheckMetricJob $job) use ($healthcheck) {\n            return $job->healthcheck->is($healthcheck)\n                && $job->runId === 7;\n        });\n    }\n\n    #[Test]\n    public function it_does_not_notify_when_all_results_are_healthy(): void\n    {\n        Bus::fake();\n        $this->fakeNotification(HealthCheckFailedNotification::class);\n\n        $healthcheck = Healthcheck::query()->create([\n            'domain' => 'example.com',\n            'type' => Type::Laravel,\n            'interval' => 5,\n            'token' => 'result-healthy-token',\n        ]);\n\n        Result::query()->create([\n            'healthcheck_id' => $healthcheck->id,\n            'key' => 'uptime',\n            'status' => Status::Healthy,\n        ]);\n\n        /** @var CheckResult $action */\n        $action = app(CheckResult::class);\n        $action->check($healthcheck, 99);\n\n        $this->assertNotificationNotDispatched(HealthCheckFailedNotification::class);\n        $refreshedHealthcheck = $healthcheck->fresh();\n        $this->assertInstanceOf(Healthcheck::class, $refreshedHealthcheck);\n        $this->assertSame(Status::Healthy, $refreshedHealthcheck->status);\n\n        Bus::assertDispatched(CheckMetricJob::class, function (CheckMetricJob $job) use ($healthcheck) {\n            return $job->healthcheck->is($healthcheck)\n                && $job->runId === 99;\n        });\n    }\n\n    /**\n     * @param  class-string<\\Vigilant\\Notifications\\Notifications\\Notification>  $notificationClass\n     */\n    private function fakeNotification(string $notificationClass): void\n    {\n        $notificationClass::fake();\n\n        $this->resetNotificationFakes($notificationClass);\n    }\n\n    /**\n     * @param  class-string<\\Vigilant\\Notifications\\Notifications\\Notification>  $notificationClass\n     */\n    private function assertNotificationNotDispatched(string $notificationClass): void\n    {\n        $this->assertEmpty(\n            $this->notificationDispatches($notificationClass),\n            \"Did not expect {$notificationClass} to be dispatched.\"\n        );\n    }\n\n    /**\n     * @param  class-string<\\Vigilant\\Notifications\\Notifications\\Notification>  $notificationClass\n     */\n    private function assertNotificationDispatched(string $notificationClass, callable $callback): void\n    {\n        $dispatches = $this->notificationDispatches($notificationClass);\n\n        $this->assertNotEmpty(\n            $dispatches,\n            \"Expected {$notificationClass} to be dispatched.\"\n        );\n\n        $matched = false;\n\n        foreach ($dispatches as $dispatch) {\n            if ($callback($dispatch)) {\n                $matched = true;\n                break;\n            }\n        }\n\n        $this->assertTrue(\n            $matched,\n            \"No dispatched {$notificationClass} matched the provided conditions.\"\n        );\n    }\n\n    /**\n     * @param  class-string<\\Vigilant\\Notifications\\Notifications\\Notification>  $notificationClass\n     * @return array<int, \\Vigilant\\Notifications\\Notifications\\Notification>\n     */\n    private function notificationDispatches(string $notificationClass): array\n    {\n        $property = new \\ReflectionProperty($notificationClass, 'fakeDispatches');\n        $property->setAccessible(true);\n\n        /** @var array<int, \\Vigilant\\Notifications\\Notifications\\Notification> $dispatches */\n        $dispatches = $property->getValue();\n\n        return $dispatches;\n    }\n\n    /**\n     * @param  class-string<\\Vigilant\\Notifications\\Notifications\\Notification>  $notificationClass\n     */\n    private function resetNotificationFakes(string $notificationClass): void\n    {\n        $property = new \\ReflectionProperty($notificationClass, 'fakeDispatches');\n        $property->setAccessible(true);\n        $property->setValue(null, []);\n    }\n}\n"
  },
  {
    "path": "packages/healthchecks/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\Healthchecks\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Livewire\\LivewireServiceProvider;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Healthchecks\\ServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n            \\Vigilant\\Core\\ServiceProvider::class,\n            \\Vigilant\\Users\\ServiceProvider::class,\n            LivewireServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n    }\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        TeamService::fake();\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/lighthouse/composer.json",
    "content": "{\n    \"name\": \"vigilant/lighthouse\",\n    \"description\": \"Vigilant Lighthouse\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"guzzlehttp/guzzle\": \"^7.8\",\n        \"laravel/framework\": \"^12.0\",\n        \"livewire/livewire\": \"^3.4\",\n        \"vigilant/core\": \"@dev\",\n        \"vigilant/sites\": \"@dev\",\n        \"vigilant/users\": \"@dev\",\n        \"vigilant/frontend\": \"@dev\",\n        \"vigilant/notifications\": \"@dev\",\n        \"geerlingguy/ping\": \"^1.2\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Lighthouse\\\\\": \"src\",\n            \"Vigilant\\\\Lighthouse\\\\Database\\\\Factories\\\\\": \"database/factories\",\n            \"Vigilant\\\\Users\\\\Database\\\\Factories\\\\\": \"../users/database/factories\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Lighthouse\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\Lighthouse\\\\ServiceProvider\"\n            ]\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/lighthouse/config/lighthouse.php",
    "content": "<?php\n\nreturn [\n    'queue' => 'lighthouse',\n\n    'intervals' => [\n        60 => 'Hourly',\n        60 * 3 => 'Every 3 hours',\n        60 * 6 => 'Every 6 hours',\n        60 * 12 => 'Every 12 hours',\n        60 * 24 => 'Daily',\n        60 * 24 * 7 => 'Weekly',\n    ],\n\n    'runs' => env('LIGHTHOUSE_RUNS', 3),\n\n    'workers' => explode(',', env('LIGHTHOUSE_WORKERS', 'lighthouse')),\n\n    /* URL to Vigilant */\n    'lighthouse_app_url' => env('LIGHTHOUSE_APP_URL', 'http://app:8000'),\n];\n"
  },
  {
    "path": "packages/lighthouse/database/migrations/2024_05_11_105500_create_lighthouse_sites_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('lighthouse_monitors', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(Site::class)->nullable()->constrained()->onDelete('cascade');\n            $table->foreignIdFor(Team::class)->constrained()->onDelete('cascade');\n\n            $table->string('url');\n\n            $table->json('settings');\n            $table->string('interval');\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('lighthouse_monitors');\n    }\n};\n"
  },
  {
    "path": "packages/lighthouse/database/migrations/2024_05_11_120000_create_lighthouse_results_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('lighthouse_results', function (Blueprint $table) {\n            $table->id();\n            $table->unsignedBigInteger('lighthouse_site_id');\n            $table->foreignIdFor(Team::class)->constrained()->onDelete('cascade');\n\n            $table->float('performance');\n            $table->float('accessibility');\n            $table->float('best_practices');\n            $table->float('seo');\n\n            $table->timestamps();\n\n            $table->foreign('lighthouse_site_id')\n                ->references('id')\n                ->on('lighthouse_monitors')\n                ->onDelete('cascade');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('lighthouse_results');\n    }\n};\n"
  },
  {
    "path": "packages/lighthouse/database/migrations/2024_05_17_073000_lighthouse_results_aggregated_field_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('lighthouse_results', function (Blueprint $table) {\n            $table->boolean('aggregated')->after('seo')->default(0);\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropColumns('lighthouse_results', ['aggregated']);\n    }\n};\n"
  },
  {
    "path": "packages/lighthouse/database/migrations/2024_06_22_160000_create_lighthouse_result_audits_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('lighthouse_result_audits', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(LighthouseResult::class)->constrained()->onDelete('cascade');\n            $table->foreignIdFor(Team::class)->constrained()->onDelete('cascade');\n\n            $table->string('audit')->index();\n            $table->string('title', 1024);\n            $table->string('explanation', 1024)->nullable();\n            $table->text('description')->nullable();\n            $table->float('score')->nullable();\n            $table->string('scoreDisplayMode');\n            $table->json('details')->nullable();\n            $table->json('warnings')->nullable();\n            $table->json('items')->nullable();\n            $table->json('metricSavings')->nullable();\n            $table->float('guidanceLevel')->nullable();\n\n            $table->float('numericValue')->nullable();\n            $table->string('numericUnit')->nullable();\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('lighthouse_result_audits');\n    }\n};\n"
  },
  {
    "path": "packages/lighthouse/database/migrations/2024_07_13_200000_lighthouse_site_rename_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        if (Schema::hasTable('lighthouse_sites')) {\n            Schema::rename('lighthouse_sites', 'lighthouse_monitors');\n        }\n\n        Schema::table('lighthouse_results', function (Blueprint $table): void {\n            $table->renameColumn('lighthouse_site_id', 'lighthouse_monitor_id');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::rename('lighthouse_monitors', 'lighthouse_sites');\n\n        Schema::table('lighthouse_results', function (Blueprint $table): void {\n            $table->renameColumn('lighthouse_monitor_id', 'lighthouse_site_id');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/lighthouse/database/migrations/2025_02_01_173000_lighthouse_monitors_enabled_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('lighthouse_monitors', function (Blueprint $table) {\n            $table->boolean('enabled')->default(true)->after('id');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropColumns('lighthouse_monitors', ['enabled']);\n    }\n};\n"
  },
  {
    "path": "packages/lighthouse/database/migrations/2025_02_03_190000_lighthouse_monitors_next_run_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        LighthouseMonitor::query()->withoutGlobalScopes()->update(['interval' => 60]);\n\n        Schema::table('lighthouse_monitors', function (Blueprint $table): void {\n            $table->integer('interval')->change();\n            $table->dateTime('next_run')->nullable()->after('interval');\n\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropColumns('lighthouse_monitors', ['next_run']);\n\n        Schema::table('lighthouse_monitors', function (Blueprint $table): void {\n            $table->string('interval')->change();\n        });\n    }\n};\n"
  },
  {
    "path": "packages/lighthouse/database/migrations/2025_02_07_210000_lighthouse_monitors_batch_fields.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('lighthouse_results', function (Blueprint $table): void {\n            $table->uuid('batch_id')->after('team_id')->nullable();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('lighthouse_results', function (Blueprint $table): void {\n            $table->dropColumn('batch_id');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/lighthouse/database/migrations/2025_03_19_200000_lighthouse_monitors_run_started_at_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('lighthouse_monitors', function (Blueprint $table): void {\n            $table->dateTime('run_started_at')->nullable()->after('next_run');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('lighthouse_monitors', function (Blueprint $table): void {\n            $table->dropColumn('run_started_at');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/lighthouse/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n    ignoreErrors:\n        - identifier: missingType.iterableValue\n        - identifier: missingType.generics\n"
  },
  {
    "path": "packages/lighthouse/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/lighthouse/resources/navigation.php",
    "content": "<?php\n\nuse Vigilant\\Core\\Facades\\Navigation;\n\nNavigation::add(route('lighthouse'), 'Lighthouse')\n    ->parent('performance')\n    ->icon('phosphor-lighthouse-light')\n    ->gate('use-lighthouse')\n    ->routeIs('lighthouse*')\n    ->sort(300);\n"
  },
  {
    "path": "packages/lighthouse/resources/views/components/average-difference.blade.php",
    "content": "@props(['difference'])\n\n@if ($difference !== null)\n    @php($differencePercent = $difference->averageDifference())\n    <span @class([\n        'text-green-light' => $differencePercent > 0,\n        'text-base-600' => $differencePercent == 0,\n        'text-red-light' => $differencePercent < 0,\n    ])>{{ round($differencePercent, 1) . '%' }}</span>\n@else\n    <span>-</span>\n@endif\n"
  },
  {
    "path": "packages/lighthouse/resources/views/components/empty-states/monitors.blade.php",
    "content": "<x-frontend::empty-state\n    :title=\"__('No Lighthouse Monitors')\"\n    :description=\"__('Set up a Lighthouse monitor to track performance, accessibility, SEO, and best practices for your critical pages.')\"\n    icon=\"phosphor-warning-circle\"\n    iconClass=\"h-12 w-12 text-orange\"\n    iconWrapperClass=\"rounded-full bg-orange/10 p-4 mb-6\"\n    :buttonHref=\"route('lighthouse.create')\"\n    :buttonText=\"__('Add Lighthouse Monitor')\"\n    buttonClass=\"bg-gradient-to-r from-orange via-yellow to-orange bg-300% hover:shadow-lg hover:shadow-orange/30 transition-all duration-300\"\n/>\n"
  },
  {
    "path": "packages/lighthouse/resources/views/lighthouse/index.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header :back=\"route('lighthouse')\" :title=\"'Lighthouse Monitor - ' .\n            $lighthouseMonitor->url .\n            ($lighthouseMonitor->enabled ? '' : ' (Disabled)')\">\n            <x-frontend::page-header.actions>\n                <x-form.button dusk=\"lighthouse-edit-button\"\n                    href=\"{{ route('lighthouse.edit', ['monitor' => $lighthouseMonitor]) }}\">\n                    @lang('Edit')\n                </x-form.button>\n                <x-form.button class=\"bg-red\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.button>\n            </x-frontend::page-header.actions>\n            \n            <x-frontend::page-header.mobile-actions>\n                <x-form.dropdown-button href=\"{{ route('lighthouse.edit', ['monitor' => $lighthouseMonitor]) }}\">\n                    @lang('Edit')\n                </x-form.dropdown-button>\n                <x-form.dropdown-button class=\"!text-red hover:!text-red-light\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.dropdown-button>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    <livewire:lighthouse-monitor-dashboard :monitorId=\"$lighthouseMonitor->id\" />\n\n    <div class=\"mt-8 grid grid-cols-1 gap-6\">\n        @foreach ($charts as $chart)\n            <div>\n                <h3 class=\"text-md font-bold leading-7 sm:truncate sm:text-xl sm:tracking-tight text-neutral-100\">\n                    {{ $chart['title'] }}</h3>\n                <p class=\"text-sm text-neutral-400 mb-4\">\n                    {{ $chart['description'] }}\n                    <br />\n                    <a href=\"{{ $chart['link'] }}\" target=\"_blank\">Learn more about the {{ $chart['title'] }}\n                        metric</a>.\n                </p>\n\n                <livewire:lighthouse-numeric-chart :audit=\"$chart['audit']\" :data=\"['lighthouseMonitorId' => $lighthouseMonitor->id]\" />\n            </div>\n        @endforeach\n    </div>\n\n    <div class=\"my-4\">\n        <h2 class=\"text-xl font-bold leading-7 sm:truncate sm:text-2xl sm:tracking-tight text-neutral-100 mb-2\">\n            {{ __('Results') }}</h2>\n        <p class=\"text-sm text-neutral-400 mb-4\">\n            @lang('View the raw results from each Lighthouse run')\n        </p>\n        <livewire:lighthouse-results-table :monitorId=\"$lighthouseMonitor->id\" />\n    </div>\n\n    @if (count($screenshots) > 0)\n        <div class=\"mt-8\">\n            <h3 class=\"text-md font-bold leading-7 sm:truncate sm:text-xl sm:tracking-tight text-neutral-100\">\n                @lang('Timeline')</h3>\n\n            <div class=\"mt-2 grid grid-cols-6 gap-4\">\n\n                @foreach ($screenshots as $screenshot)\n                    <div class=\"text-center\">\n                        <img src=\"{{ $screenshot['data'] }}\" />\n                        <span class=\"text-xs text-neutral-200\">{{ $screenshot['timing'] }}ms</span>\n                    </div>\n                @endforeach\n            </div>\n        </div>\n    @endif\n\n    <!-- Delete Confirmation Modal -->\n    <div x-data=\"{ showDeleteModal: false }\" @open-delete-modal.window=\"showDeleteModal = true\">\n        <x-frontend::modal show=\"showDeleteModal\">\n            <x-frontend::modal.header icon=\"phosphor-trash\" iconColor=\"red\" show=\"showDeleteModal\">\n                @lang('Delete Lighthouse Monitor')\n            </x-frontend::modal.header>\n\n            <x-frontend::modal.body>\n                <div class=\"space-y-4\">\n                    <p class=\"text-base-100\">\n                        @lang('Are you sure you want to delete this Lighthouse monitor?')\n                    </p>\n                    <div class=\"bg-base-850 border border-base-700 rounded-lg p-4\">\n                        <div class=\"flex items-start gap-3\">\n                            <div class=\"flex-shrink-0\">\n                                @svg('phosphor-warning-circle', 'w-5 h-5 text-orange mt-0.5')\n                            </div>\n                            <div class=\"flex-1\">\n                                <p class=\"text-sm text-base-300\">\n                                    <span class=\"font-semibold text-base-100\">{{ $lighthouseMonitor->url }}</span>\n                                </p>\n                                <p class=\"text-sm text-base-400 mt-1\">\n                                    @lang('This action cannot be undone. All performance data and reports for this monitor will be permanently deleted.')\n                                </p>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </x-frontend::modal.body>\n\n            <x-frontend::modal.footer>\n                <x-form.button type=\"button\" @click=\"showDeleteModal = false\">\n                    @lang('Cancel')\n                </x-form.button>\n                <form action=\"{{ route('lighthouse.delete', ['monitor' => $lighthouseMonitor]) }}\" method=\"POST\" class=\"inline\">\n                    @csrf\n                    @method('DELETE')\n                    <x-form.button class=\"bg-red\" type=\"submit\">\n                        @lang('Delete Monitor')\n                    </x-form.button>\n                </form>\n            </x-frontend::modal.footer>\n        </x-frontend::modal>\n    </div>\n\n</x-app-layout>\n"
  },
  {
    "path": "packages/lighthouse/resources/views/livewire/lighthouse-site-form.blade.php",
    "content": "<div>\n    @if (!$inline)\n        <x-slot name=\"header\">\n            <x-page-header :title=\"$updating ? 'Edit Lighthouse Monitor - ' . $lighthouseMonitor->url : 'Add Lighthouse Monitor'\" :back=\"$updating ? route('lighthouse.index', ['monitor' => $lighthouseMonitor]) : route('lighthouse')\">\n            </x-page-header>\n        </x-slot>\n    @endif\n\n    <form wire:submit=\"save\">\n        <div class=\"max-w-7xl mx-auto\">\n            <x-card>\n                <div class=\"flex flex-col gap-4\">\n                    @if (!$inline)\n                        <x-form.checkbox field=\"form.enabled\" name=\"Enabled\"\n                            description=\"Enable or disable this lighthouse monitor\" />\n                    @endif\n                    <x-form.text field=\"form.url\" name=\"URL\" description=\"Site URL\" />\n\n                    <x-form.select field=\"form.interval\" name=\"Interval\"\n                        description=\"Choose how often this monitor should check the lighthouse scores\">\n                        @foreach (config('lighthouse.intervals') as $interval => $label)\n                            <option value=\"{{ $interval }}\">@lang($label)</option>\n                        @endforeach\n                    </x-form.select>\n\n                    @if (!$inline)\n                        <x-form.submit-button dusk=\"submit-button\" :submitText=\"$updating ? 'Save' : 'Create'\" />\n                    @endif\n                </div>\n            </x-card>\n        </div>\n    </form>\n</div>\n"
  },
  {
    "path": "packages/lighthouse/resources/views/livewire/lighthouse-sites.blade.php",
    "content": "<div>\n    <x-slot name=\"header\">\n        <x-page-header title=\"Lighthouse Monitoring\">\n            <x-frontend::page-header.actions>\n                <x-create-button dusk=\"lighthouse-add-button\" :href=\"route('lighthouse.create')\"\n                    model=\"Vigilant\\Lighthouse\\Models\\LighthouseMonitor\">\n                    @lang('Add Lighthouse Monitor')\n                </x-create-button>\n            </x-frontend::page-header.actions>\n            <x-frontend::page-header.mobile-actions>\n                <x-create-button-dropdown :href=\"route('lighthouse.create')\" model=\"Vigilant\\Lighthouse\\Models\\LighthouseMonitor\">\n                    @lang('Add Lighthouse Monitor')\n                </x-create-button-dropdown>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    @if ($hasMonitors)\n        <livewire:lighthouse-sites-table />\n    @else\n        <x-lighthouse::empty-states.monitors />\n    @endif\n</div>\n"
  },
  {
    "path": "packages/lighthouse/resources/views/livewire/monitor/dashboard.blade.php",
    "content": "<div class=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n\n    <dl class=\"grid grid-cols-2 lg:grid-cols-4 gap-4\">\n        @foreach (['performance', 'accessibility', 'best_practices', 'seo'] as $category)\n            @php\n                $color = 'text-red';\n\n                if ($lastResult !== null) {\n                    $percentage = round($lastResult[$category] * 100);\n\n                    $color = match (true) {\n                        $percentage > 80 => 'text-green-light',\n                        $percentage > 60 => 'text-orange-light',\n                        default => 'text-red-light',\n                    };\n                }\n\n            @endphp\n            <x-frontend::stats-card :title=\"__(str_replace('_', ' ', ucfirst($category)))\">\n                {{ $lastResult === null ? __('-') : $percentage . '%' }}\n            </x-frontend::stats-card>\n        @endforeach\n\n        @foreach (['7d' => 'Week', '30d' => 'Month', '90d' => '3 Months', '180d' => '6 Months'] as $timeframe => $label)\n            <x-frontend::stats-card :title=\"__($label)\">\n                <x-lighthouse::average-difference :difference=\"$difference[$timeframe]\" />\n            </x-frontend::stats-card>\n        @endforeach\n    </dl>\n\n    <div class=\"flex-1\">\n        <livewire:lighthouse-categories-chart :data=\"['lighthouseMonitorId' => $lighthouseMonitor->id]\" />\n    </div>\n\n</div>\n"
  },
  {
    "path": "packages/lighthouse/resources/views/result/index.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header :back=\"route('lighthouse.index', ['monitor' => $result->lighthouse_monitor_id])\" title=\"Lighthouse Result - {{ $result->created_at->toDateTimeString('minute') }}\">\n\n        </x-page-header>\n    </x-slot>\n\n    <div class=\"flex\">\n        <dl class=\"grid grid-cols-2 lg:grid-cols-4 gap-4 w-full\">\n            @foreach(['performance', 'accessibility', 'best_practices', 'seo'] as $category)\n                @php\n                    $color = 'text-red';\n\n                    $percentage = round($result[$category] * 100);\n\n                    $color = match(true) {\n                        $percentage > 80 => 'text-green-light',\n                        $percentage > 60 => 'text-orange-light',\n                        default => 'text-red-light'\n                    };\n\n                @endphp\n                <div class=\"text-base-50 bg-base-950 text-center p-4 rounded-sm shadow-sm\">\n                    <dt class=\"truncate text-sm font-medium text-base-100\">{{ str_replace('_', ' ', ucfirst($category)) }}</dt>\n                    <dd class=\"mt-1 text-xl font-semibold tracking-tight {{ $color ?? 'text-base-50' }}\">{{  $percentage . '%' }}</dd>\n                </div>\n            @endforeach\n        </dl>\n    </div>\n\n    <div class=\"\">\n        <h2 class=\"text-xl font-bold leading-7 sm:truncate sm:text-2xl sm:tracking-tight text-neutral-100 mb-2\">{{ __('Audits') }}</h2>\n\n        <livewire:lighthouse-result-audits-table :resultId=\"$result->id\"/>\n    </div>\n</x-app-layout>\n"
  },
  {
    "path": "packages/lighthouse/routes/api.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\Lighthouse\\Http\\Controllers\\LighthouseCallbackController;\n\nRoute::post('/callback/{monitorId}/{batch}/{worker}', [LighthouseCallbackController::class, 'result'])\n    ->middleware('signed:relative')\n    ->name('lighthouse.callback');\n"
  },
  {
    "path": "packages/lighthouse/routes/web.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\Lighthouse\\Http\\Controllers\\LighthouseMonitorController;\nuse Vigilant\\Lighthouse\\Http\\Controllers\\LighthouseResultController;\nuse Vigilant\\Lighthouse\\Livewire\\LighthouseSiteForm;\nuse Vigilant\\Lighthouse\\Livewire\\LighthouseSites;\n\nRoute::prefix('lighthouse')\n    ->middleware('can:use-lighthouse')\n    ->group(function (): void {\n        Route::get('/', LighthouseSites::class)->name('lighthouse');\n        Route::get('/create', LighthouseSiteForm::class)->name('lighthouse.create');\n        Route::get('/{monitor}', [LighthouseMonitorController::class, 'index'])->name('lighthouse.index')->can('view,monitor');\n        Route::delete('/{monitor}', [LighthouseMonitorController::class, 'delete'])->name('lighthouse.delete')->can('delete,monitor');\n        Route::get('/{monitor}/edit', LighthouseSiteForm::class)->name('lighthouse.edit');\n        Route::get('/ressult/{result}', [LighthouseResultController::class, 'index'])->name('lighthouse.result.index');\n    });\n"
  },
  {
    "path": "packages/lighthouse/src/Actions/AggregateLighthouseBatch.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Actions;\n\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Lighthouse\\Jobs\\CheckLighthouseResultJob;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResultAudit;\n\nclass AggregateLighthouseBatch\n{\n    public function aggregateBatch(LighthouseMonitor $monitor, string $batchId): void\n    {\n        /** @var Collection<int, LighthouseResult> $results */\n        $results = $monitor->lighthouseResults()->where('batch_id', '=', $batchId)->get();\n\n        if ($results->isEmpty()) {\n            return;\n        }\n\n        if ($results->count() === 1) {\n            CheckLighthouseResultJob::dispatch($results->first());\n\n            return;\n        }\n\n        /** @var LighthouseResult $resultAverages */\n        $resultAverages = $monitor->lighthouseResults()\n            ->where('batch_id', '=', $batchId)\n            ->groupBy('batch_id')\n            ->selectRaw('AVG(performance) as performance, AVG(accessibility) as accessibility, AVG(best_practices) as best_practices, AVG(seo) as seo')\n            ->first();\n\n        /** @var LighthouseResult $newResult */\n        $newResult = $monitor->lighthouseResults()->create([\n            'performance' => $resultAverages->performance,\n            'accessibility' => $resultAverages->accessibility,\n            'best_practices' => $resultAverages->best_practices,\n            'seo' => $resultAverages->seo,\n        ]);\n\n        /** @var LighthouseResult $firstResult */\n        $firstResult = $monitor->lighthouseResults()->where('batch_id', '=', $batchId)->first();\n\n        $auditCategories = $firstResult->audits()->select('audit')->distinct()->get()->pluck('audit')->toArray();\n\n        foreach ($auditCategories as $audit) {\n\n            /** @var ?LighthouseResultAudit $averages */\n            $averages = LighthouseResultAudit::query()\n                ->whereIn('lighthouse_result_id', $results->pluck('id'))\n                ->where('audit', '=', $audit)\n                ->selectRaw('AVG(score) as score, AVG(numericValue) as numericValue')\n                ->first();\n\n            /** @var ?LighthouseResultAudit $allValues */\n            $allValues = LighthouseResultAudit::query()\n                ->whereIn('lighthouse_result_id', $results->pluck('id'))\n                ->where('audit', '=', $audit)\n                ->first();\n\n            if ($averages === null || $allValues === null) {\n                continue;\n            }\n\n            $newResult->audits()->create(array_merge([\n                'audit' => $audit,\n                'score' => $averages->score,\n                'numericValue' => $averages->numericValue,\n            ], $allValues->only(['title', 'explanation', 'description', 'scoreDisplayMode', 'details', 'warnings', 'items', 'metricSavings', 'guidanceLevel', 'numericUnit'])));\n        }\n\n        $monitor->lighthouseResults()->where('batch_id', '=', $batchId)->delete();\n\n        CheckLighthouseResultJob::dispatch($newResult);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Actions/AggregateResults.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Actions;\n\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResultAudit;\n\nclass AggregateResults\n{\n    public function aggregate(LighthouseMonitor $site, Carbon $from, Carbon $till): void\n    {\n        $results = $site->lighthouseResults()\n            ->where('aggregated', '=', false)\n            ->where('created_at', '>=', $from)\n            ->where('created_at', '<=', $till)\n            ->get();\n\n        if ($results->isEmpty()) {\n            return;\n        }\n\n        /** @var LighthouseResult $aggregate */\n        $aggregate = $results->first();\n\n        $aggregate->performance = round($results->average('performance') ?? 0, 2);\n        $aggregate->accessibility = round($results->average('accessibility') ?? 0, 2);\n        $aggregate->best_practices = round($results->average('best_practices') ?? 0, 2);\n        $aggregate->seo = round($results->average('seo') ?? 0, 2);\n        $aggregate->aggregated = true;\n        $aggregate->save();\n\n        $idsToDelete = $results\n            ->where('id', '!=', $aggregate->id)\n            ->pluck('id')\n            ->toArray();\n\n        LighthouseResultAudit::query()\n            ->whereIn('lighthouse_result_id', $idsToDelete)\n            ->delete();\n\n        LighthouseResult::query()\n            ->whereIn('id', $idsToDelete)\n            ->delete();\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Actions/CalculateTimeDifference.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Actions;\n\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Lighthouse\\Data\\CategoryResultDifferenceData;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\n\nclass CalculateTimeDifference\n{\n    public function calculate(LighthouseMonitor $monitor, Carbon $from, float $sampleSize = 0.1): ?CategoryResultDifferenceData\n    {\n        // Ensure that there are enough results to compare\n        if ($monitor->lighthouseResults()->where('created_at', '<=', $from)->count() === 0) {\n            return null;\n        }\n\n        $results = $monitor->lighthouseResults()\n            ->where('created_at', '>=', $from)\n            ->get();\n\n        if ($results->isEmpty()) {\n            return null;\n        }\n\n        /** @var LighthouseResult $firstResult */\n        $firstResult = $results->sortBy('created_at')->first();\n\n        if ($firstResult->created_at === null || $from->diffInDays($firstResult->created_at) > 7) {\n            return null;\n        }\n\n        $take = (int) max(1, round($results->count() * $sampleSize));\n\n        $old = $results->take($take);\n        $new = $results->skip($results->count() - $take)->take($take);\n\n        return CategoryResultDifferenceData::of([\n            'performance_old' => $old->average('performance'),\n            'performance_new' => $new->average('performance'),\n            'accessibility_old' => $old->average('accessibility'),\n            'accessibility_new' => $new->average('accessibility'),\n            'best_practices_old' => $old->average('best_practices'),\n            'best_practices_new' => $new->average('best_practices'),\n            'seo_old' => $old->average('seo'),\n            'seo_new' => $new->average('seo'),\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Actions/CheckLighthouseResult.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Actions;\n\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Lighthouse\\Data\\CategoryResultDifferenceData;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResultAudit;\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\n\nclass CheckLighthouseResult\n{\n    protected array $categories = [\n        'performance',\n        'accessibility',\n        'best_practices',\n        'seo',\n    ];\n\n    public function __construct(protected CheckLighthouseResultAudit $checkLighthouseResultAudit) {}\n\n    public function check(LighthouseResult $result): void\n    {\n        $totalResultCount = LighthouseResult::query()\n            ->where('lighthouse_monitor_id', '=', $result->lighthouse_monitor_id)\n            ->count();\n\n        // Not enough data\n        if ($totalResultCount < 10) {\n            return;\n        }\n\n        // take 10% of the result set to calculate the current value\n        $currentLimit = (int) floor($totalResultCount * 0.1);\n\n        // take 30% of the result set before the current to calculate the previous value\n        $previousLimit = (int) floor($totalResultCount * 0.3);\n\n        $current = $this->averageResults($result->lighthouse_monitor_id, $currentLimit, 0)\n            ->mapWithKeys(fn (?float $score, string $key) => [$key.'_new' => $score ?? 0]);\n\n        $previous = $this->averageResults($result->lighthouse_monitor_id, $previousLimit, $currentLimit)\n            ->mapWithKeys(fn (?float $score, string $key) => [$key.'_old' => $score ?? 0]);\n\n        $data = CategoryResultDifferenceData::of($current->merge($previous)->toArray());\n\n        CategoryScoreChangedNotification::notify($result, $data);\n\n        /** @var Collection<int, LighthouseResultAudit> $audits */\n        $audits = $result->audits()->get();\n\n        $audits->each(fn (LighthouseResultAudit $audit) => $this->checkLighthouseResultAudit->check($audit));\n    }\n\n    protected function averageResults(int $lighthouseSiteId, int $count, int $skip): Collection\n    {\n        $results = LighthouseResult::query()\n            ->where('lighthouse_monitor_id', '=', $lighthouseSiteId)\n            ->orderByDesc('id')\n            ->skip($skip)\n            ->take($count)\n            ->get();\n\n        return collect($this->categories)\n            ->mapWithKeys(fn (string $category): array => [$category => $results->average($category)]);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Actions/CheckLighthouseResultAudit.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Actions;\n\nuse Illuminate\\Database\\Query\\JoinClause;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResultAudit;\nuse Vigilant\\Lighthouse\\Notifications\\NumericAuditChangedNotification;\n\nclass CheckLighthouseResultAudit\n{\n    public function check(LighthouseResultAudit $audit): void\n    {\n        if ($audit->scoreDisplayMode !== 'numeric') {\n            return;\n        }\n\n        /** @var ?int $monitorId */\n        $monitorId = $audit->lighthouseResult?->lighthouse_monitor_id;\n\n        throw_if($monitorId === null, 'Invalid relationship');\n\n        $totalResultCount = LighthouseResultAudit::query()\n            ->join('lighthouse_results', function (JoinClause $join) use ($monitorId) {\n                $join->on('lighthouse_results.id', '=', 'lighthouse_result_audits.lighthouse_result_id')\n                    ->where('lighthouse_results.lighthouse_monitor_id', '=', $monitorId)\n                    ->where('lighthouse_results.created_at', '>', now()->subMonth());\n            })\n            ->where('audit', '=', $audit->audit)\n            ->count();\n\n        // Not enough data\n        if ($totalResultCount < 10) {\n            return;\n        }\n\n        // take 10% of the result set to calculate the current value\n        $currentLimit = (int) floor($totalResultCount * 0.1);\n\n        // take 30% of the result set before the current to calculate the previous value\n        $previousLimit = (int) floor($totalResultCount * 0.3);\n\n        $current = $this->averageNumericValue($audit, $currentLimit, 0);\n\n        $previous = $this->averageNumericValue($audit, $previousLimit, $currentLimit);\n\n        if ($previous == 0) {\n            $percentDifference = ($current == 0) ? 0 : 100;\n        } else {\n            $percentDifference = (($current - $previous) / $previous) * 100;\n        }\n\n        if ($percentDifference > 0) {\n            NumericAuditChangedNotification::notify($audit, $percentDifference, $previous, $current);\n        }\n    }\n\n    protected function averageNumericValue(LighthouseResultAudit $audit, int $count = 3, int $skip = 0): float\n    {\n        /** @var ?int $monitorId */\n        $monitorId = $audit->lighthouseResult?->lighthouse_monitor_id;\n\n        throw_if($monitorId === null, 'Invalid relationship');\n\n        return (float) LighthouseResultAudit::query()\n            ->join('lighthouse_results', function (JoinClause $join) use ($monitorId) {\n                $join->on('lighthouse_results.id', '=', 'lighthouse_result_audits.lighthouse_result_id')\n                    ->where('lighthouse_results.lighthouse_monitor_id', '=', $monitorId);\n            })\n            ->where('audit', '=', $audit->audit)\n            ->orderByDesc('lighthouse_result_audits.id')\n            ->skip($skip)\n            ->take($count)\n            ->get()\n            ->average('numericValue');\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Actions/ProcessLighthouseResult.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Actions;\n\nuse Vigilant\\Lighthouse\\Jobs\\AggregateLighthouseBatchJob;\nuse Vigilant\\Lighthouse\\Jobs\\RunLighthouseJob;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\n\nclass ProcessLighthouseResult\n{\n    public function process(LighthouseMonitor $monitor, string $batchId, array $result): void\n    {\n        /** @var array<string, array> $categoriesResult */\n        $categoriesResult = $result['categories'] ?? [];\n\n        /** @var array<string, array> $audits */\n        $audits = $result['audits'];\n\n        $categories = collect($categoriesResult)\n            ->mapWithKeys(function (array $result, string $key): array {\n                return [str_replace('-', '_', $key) => $result['score']];\n            })\n            ->toArray();\n\n        /** @var LighthouseResult $result */\n        $result = $monitor->lighthouseResults()->create(array_merge(['batch_id' => $batchId], $categories));\n\n        foreach ($audits as $audit) {\n            $result->audits()->create([\n                'audit' => $audit['id'],\n                'title' => $audit['title'],\n                'explanation' => $audit['explanation'] ?? null,\n                'description' => $audit['description'] ?? null,\n                'score' => $audit['score'] ?? null,\n                'scoreDisplayMode' => $audit['scoreDisplayMode'],\n                'details' => $audit['details'] ?? null,\n                'warnings' => $audit['warnings'] ?? null,\n                'items' => $audit['items'] ?? null,\n                'metricSavings' => $audit['metricSavings'] ?? null,\n                'guidanceLevel' => $audit['guidanceLevel'] ?? null,\n                'numericValue' => $audit['numericValue'] ?? null,\n                'numericUnit' => $audit['numericUnit'] ?? null,\n            ]);\n        }\n\n        $batchCount = $monitor->lighthouseResults()\n            ->where('batch_id', $batchId)\n            ->count();\n\n        /** @var int $lighthouseRuns */\n        $lighthouseRuns = config('lighthouse.runs');\n\n        if ($batchCount >= $lighthouseRuns) {\n            AggregateLighthouseBatchJob::dispatch($monitor, $batchId);\n\n            $monitor->update([\n                'run_started_at' => null,\n            ]);\n\n        } else {\n            RunLighthouseJob::dispatch($monitor, $batchId);\n        }\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Actions/RunLighthouse.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Actions;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Facades\\URL;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\n\nclass RunLighthouse\n{\n    public function __construct(protected CheckLighthouseResult $lighthouseResult) {}\n\n    public function run(LighthouseMonitor $monitor, ?string $batchId): void\n    {\n        $worker = $this->getAvailableWorker();\n\n        if ($worker === null) {\n            logger()->warning('No available workers to run Lighthouse job');\n\n            return;\n        }\n\n        if ($batchId === null) {\n            $batchId = str()->uuid();\n\n            $monitor->update([\n                'next_run' => now()->addMinutes($monitor->interval),\n            ]);\n        }\n\n        $vigilantUrl = config()->string('lighthouse.lighthouse_app_url');\n\n        Http::baseUrl($worker)\n            ->post('lighthouse', [\n                'website' => $monitor->url,\n                'callback_url' => $vigilantUrl.URL::signedRoute('lighthouse.callback', ['monitorId' => $monitor->id, 'batch' => $batchId, 'worker' => $worker], absolute: false),\n            ])\n            ->throw();\n\n        $monitor->update([\n            'run_started_at' => now(),\n        ]);\n    }\n\n    public function getAvailableWorker(): ?string\n    {\n        $lockKey = 'lighthouse:worker:lock';\n        $workers = config()->array('lighthouse.workers');\n\n        $lock = cache()->lock($lockKey, 5);\n\n        if (! $lock->get()) {\n            return null; // Another process is selecting a worker\n        }\n\n        try {\n            foreach ($workers as $worker) {\n                $workerCacheKey = 'lighthouse:worker:'.$worker;\n\n                if (cache()->has($workerCacheKey)) {\n                    continue;\n                }\n\n                cache()->put($workerCacheKey, true, now()->addMinutes(5));\n\n                return $worker;\n            }\n        } finally {\n            $lock->release();\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Commands/AggregateLighthouseBatchCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Lighthouse\\Actions\\AggregateLighthouseBatch;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\n\nclass AggregateLighthouseBatchCommand extends Command\n{\n    protected $signature = 'lighthouse:aggregate-batch {resultId}';\n\n    protected $description = 'Check a lighthouse result';\n\n    public function handle(AggregateLighthouseBatch $aggregator, TeamService $teamService): int\n    {\n        /** @var int $resultId */\n        $resultId = (int) $this->argument('resultId');\n\n        /** @var LighthouseResult $result */\n        $result = LighthouseResult::query()\n            ->withoutGlobalScopes()\n            ->whereNotNull('batch_id')\n            ->where('id', '=', $resultId)\n            ->firstOrFail();\n\n        $teamService->setTeamById($result->team_id);\n\n        $aggregator->aggregateBatch($result->lighthouseSite, $result->batch_id); // @phpstan-ignore-line\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Commands/AggregateLighthouseResultsCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Lighthouse\\Jobs\\AggregateLighthouseResultsJob;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\n\nclass AggregateLighthouseResultsCommand extends Command\n{\n    protected $signature = 'lighthouse:aggregate-results';\n\n    protected $description = 'Aggregate Lighthouse result data';\n\n    public function handle(): int\n    {\n        $sites = LighthouseMonitor::query()\n            ->withoutGlobalScopes()\n            ->get();\n\n        /** @var LighthouseMonitor $site */\n        foreach ($sites as $site) {\n            /** @var ?LighthouseResult $lastNonAggregatedResult */\n            $lastNonAggregatedResult = $site->lighthouseResults()\n                ->withoutGlobalScopes()\n                ->where('aggregated', '=', false)\n                ->where('created_at', '<', now()->toDateString())\n                ->orderBy('created_at')\n                ->first();\n\n            if ($lastNonAggregatedResult === null) {\n                continue;\n            }\n\n            $days = round($lastNonAggregatedResult->created_at?->diffInDays(now()) ?? 0);\n\n            $start = $lastNonAggregatedResult->created_at;\n\n            if ($start === null) {\n                continue;\n            }\n\n            for ($i = 0; $i < $days; $i++) {\n\n                $end = $start->clone()->addDay();\n\n                $this->info(\"Aggregating for site {$site->id} from {$start->toDateString()} to {$end->toDateString()}\");\n\n                AggregateLighthouseResultsJob::dispatch($site, $start, $end);\n\n                $start->addDay();\n\n                if ($start->diffInDays(now()) <= 2) {\n                    break;\n                }\n            }\n        }\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Commands/CheckLighthouseCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Lighthouse\\Actions\\CheckLighthouseResult;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\n\nclass CheckLighthouseCommand extends Command\n{\n    protected $signature = 'lighthouse:check {resultId}';\n\n    protected $description = 'Check a lighthouse result';\n\n    public function handle(CheckLighthouseResult $lighthouseResult, TeamService $teamService): int\n    {\n        /** @var int $resultId */\n        $resultId = (int) $this->argument('resultId');\n\n        /** @var LighthouseResult $result */\n        $result = LighthouseResult::query()->withoutGlobalScopes()->findOrFail($resultId);\n\n        $teamService->setTeamById($result->team_id);\n\n        $lighthouseResult->check($result);\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Commands/LighthouseCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Lighthouse\\Jobs\\RunLighthouseJob;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\n\nclass LighthouseCommand extends Command\n{\n    protected $signature = 'lighthouse {siteId}';\n\n    protected $description = 'Run lighthouse for a site';\n\n    public function handle(): int\n    {\n        /** @var int $siteId */\n        $siteId = $this->argument('siteId');\n\n        /** @var LighthouseMonitor $site */\n        $site = LighthouseMonitor::query()\n            ->withoutGlobalScopes()\n            ->findOrFail($siteId);\n\n        RunLighthouseJob::dispatch($site);\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Commands/ScheduleLighthouseCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Foundation\\Bus\\PendingDispatch;\nuse Vigilant\\Lighthouse\\Jobs\\RunLighthouseJob;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\n\nclass ScheduleLighthouseCommand extends Command\n{\n    protected $signature = 'lighthouse:schedule';\n\n    protected $description = 'Schedule Lighthouse Jobs';\n\n    public function handle(): int\n    {\n        LighthouseMonitor::query()\n            ->withoutGlobalScopes()\n            ->where('enabled', '=', true)\n            ->where(function (Builder $query): void {\n                $query\n                    ->whereNull('run_started_at')\n                    ->orWhere('run_started_at', '<=', now()->subHour());\n            })\n            ->where(function (Builder $query): void {\n                $query\n                    ->whereNull('next_run')\n                    ->orWhere('next_run', '<=', now());\n            })\n            ->get()\n            ->each(fn (LighthouseMonitor $monitor): PendingDispatch => RunLighthouseJob::dispatch($monitor));\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Data/CategoryResultDifferenceData.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Data;\n\nuse Vigilant\\Core\\Data\\Data;\n\nclass CategoryResultDifferenceData extends Data\n{\n    public array $rules = [\n        'performance_old' => ['required', 'numeric'],\n        'performance_new' => ['required', 'numeric'],\n\n        'accessibility_old' => ['required', 'numeric'],\n        'accessibility_new' => ['required', 'numeric'],\n\n        'best_practices_old' => ['required', 'numeric'],\n        'best_practices_new' => ['required', 'numeric'],\n\n        'seo_old' => ['required', 'numeric'],\n        'seo_new' => ['required', 'numeric'],\n    ];\n\n    public function performanceOld(): float\n    {\n        return $this['performance_old'];\n    }\n\n    public function performanceNew(): float\n    {\n        return $this['performance_new'];\n    }\n\n    public function performanceDifference(): float\n    {\n        return $this->calculateDifference($this->performanceOld(), $this->performanceNew());\n    }\n\n    public function accessibilityOld(): float\n    {\n        return $this['accessibility_old'];\n    }\n\n    public function accessibilityNew(): float\n    {\n        return $this['accessibility_new'];\n    }\n\n    public function accessibilityDifference(): float\n    {\n        return $this->calculateDifference($this->accessibilityOld(), $this->accessibilityNew());\n    }\n\n    public function bestPracticesOld(): float\n    {\n        return $this['best_practices_old'];\n    }\n\n    public function bestPracticesNew(): float\n    {\n        return $this['best_practices_new'];\n    }\n\n    public function bestPracticesDifference(): float\n    {\n        return $this->calculateDifference($this->bestPracticesOld(), $this->bestPracticesNew());\n    }\n\n    public function seoOld(): float\n    {\n        return $this['seo_old'];\n    }\n\n    public function seoNew(): float\n    {\n        return $this['seo_new'];\n    }\n\n    public function seoDifference(): float\n    {\n        return $this->calculateDifference($this->seoOld(), $this->seoNew());\n    }\n\n    public function averageDifference(): float\n    {\n        return (float) collect([\n            $this->performanceDifference(),\n            $this->accessibilityDifference(),\n            $this->bestPracticesDifference(),\n            $this->seoDifference(),\n        ])->average();\n    }\n\n    protected function calculateDifference(float $old, float $new): float\n    {\n        if ($old == 0) {\n            return 0;\n        }\n\n        $difference = (($new - $old) / $old) * 100;\n\n        return round($difference, 1);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Http/Controllers/LighthouseCallbackController.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Http\\Controllers;\n\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Lighthouse\\Actions\\ProcessLighthouseResult;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\n\nclass LighthouseCallbackController extends Controller\n{\n    public function result(\n        int $monitorId,\n        string $batchId,\n        string $worker,\n        Request $request,\n        ProcessLighthouseResult $processor,\n        TeamService $teamService\n    ): void {\n        cache()->forget('lighthouse:worker:'.$worker);\n\n        $monitor = LighthouseMonitor::query()\n            ->withoutGlobalScopes()\n            ->findOrFail($monitorId);\n\n        $teamService->setTeamById($monitor->team_id);\n        $result = $request->only(['categories', 'audits']);\n\n        $processor->process($monitor, $batchId, $result);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Http/Controllers/LighthouseMonitorController.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Http\\Controllers;\n\nuse Illuminate\\Routing\\Controller;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResultAudit;\n\nclass LighthouseMonitorController extends Controller\n{\n    use DisplaysAlerts;\n\n    public function index(LighthouseMonitor $monitor): mixed\n    {\n        $lastResults = $monitor->lighthouseResults()->get();\n\n        /** @var ?LighthouseResult $lastResult */\n        $lastResult = $lastResults->last();\n\n        if ($lastResult !== null) {\n            /** @var ?LighthouseResultAudit $screenshotAudit */\n            $screenshotAudit = $lastResult->audits()\n                ->firstWhere('audit', '=', 'screenshot-thumbnails');\n\n            if ($screenshotAudit !== null) {\n                $screenshots = $screenshotAudit->details['items'] ?? [];\n            }\n        }\n\n        /** @var view-string $view */\n        $view = 'lighthouse::lighthouse.index';\n\n        return view($view, [\n            'lighthouseMonitor' => $monitor,\n            'screenshots' => $screenshots ?? [],\n            'charts' => [\n                [\n                    'audit' => 'first-contentful-paint',\n                    'title' => 'First Contentful Paint',\n                    'description' => 'First Contentful Paint marks the time at which the first text or image is painted.',\n                    'link' => 'https://developer.chrome.com/docs/lighthouse/performance/first-contentful-paint/',\n                ],\n                [\n                    'audit' => 'largest-contentful-paint',\n                    'title' => 'Largest Contentful Paint',\n                    'description' => 'Largest Contentful Paint marks the time at which the largest text or image is painted.',\n                    'link' => 'https://developer.chrome.com/docs/lighthouse/performance/lighthouse-largest-contentful-paint/',\n                ],\n                [\n                    'audit' => 'speed-index',\n                    'title' => 'Speed Index',\n                    'description' => ' Speed Index shows how quickly the contents of a page are visibly populated.',\n                    'link' => 'https://developer.chrome.com/docs/lighthouse/performance/speed-index/',\n                ],\n                [\n                    'audit' => 'interactive',\n                    'title' => 'Time to Interactive',\n                    'description' => 'Time to Interactive is the amount of time it takes for the page to become fully interactive.',\n                    'link' => 'https://developer.chrome.com/docs/lighthouse/performance/interactive/',\n                ],\n                [\n                    'audit' => 'total-blocking-time',\n                    'title' => 'Total Blocking Time',\n                    'description' => 'Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds.',\n                    'link' => 'https://developer.chrome.com/docs/lighthouse/performance/lighthouse-total-blocking-time/',\n                ],\n                [\n                    'audit' => 'cumulative-layout-shift',\n                    'title' => 'Cumulative Layout Shift',\n                    'description' => 'Cumulative Layout Shift measures the movement of visible elements within the viewport.',\n                    'link' => 'https://web.dev/articles/cls',\n                ],\n\n            ],\n        ]);\n    }\n\n    public function delete(LighthouseMonitor $monitor): mixed\n    {\n        $monitor->delete();\n\n        $this->alert(\n            __('Deleted'),\n            __('Lighthouse monitor was successfully deleted'),\n            AlertType::Success\n        );\n\n        return response()->redirectToRoute('lighthouse');\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Http/Controllers/LighthouseResultController.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Http\\Controllers;\n\nuse Illuminate\\Contracts\\View\\View;\nuse Illuminate\\Routing\\Controller;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\n\nclass LighthouseResultController extends Controller\n{\n    public function index(LighthouseResult $result): View\n    {\n        /** @var view-string $view */\n        $view = 'lighthouse::result.index';\n\n        return view($view, [\n            'result' => $result,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Jobs/AggregateLighthouseBatchJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Lighthouse\\Actions\\AggregateLighthouseBatch;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\n\nclass AggregateLighthouseBatchJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(public LighthouseMonitor $site, public string $batchId)\n    {\n        $this->onQueue(config('lighthouse.queue'));\n    }\n\n    public function handle(TeamService $teamService, AggregateLighthouseBatch $aggregator): void\n    {\n        $teamService->setTeamById($this->site->team_id);\n\n        $aggregator->aggregateBatch($this->site, $this->batchId);\n\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->site->id;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Jobs/AggregateLighthouseResultsJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Lighthouse\\Actions\\AggregateResults;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\n\nclass AggregateLighthouseResultsJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(\n        public LighthouseMonitor $site,\n        public Carbon $from,\n        public Carbon $till,\n    ) {\n        $this->onQueue(config('lighthouse.queue'));\n    }\n\n    public function handle(AggregateResults $aggregateResults, TeamService $teamService): void\n    {\n        $teamService->setTeamById($this->site->team_id);\n        $aggregateResults->aggregate(\n            $this->site,\n            $this->from,\n            $this->till\n        );\n    }\n\n    public function uniqueId(): string\n    {\n        return $this->site->id.$this->from->getTimestamp();\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Jobs/CheckLighthouseResultJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Lighthouse\\Actions\\CheckLighthouseResult;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\n\nclass CheckLighthouseResultJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(public LighthouseResult $result)\n    {\n        $this->onQueue(config('lighthouse.queue'));\n    }\n\n    public function handle(CheckLighthouseResult $checker, TeamService $teamService): void\n    {\n        $teamService->setTeamById($this->result->team_id);\n        $checker->check($this->result);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->result->id;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Jobs/RunLighthouseJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUniqueUntilProcessing;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Lighthouse\\Actions\\RunLighthouse;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\n\nclass RunLighthouseJob implements ShouldBeUniqueUntilProcessing, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(public LighthouseMonitor $site, public ?string $batchId = null)\n    {\n        $this->onQueue(config('lighthouse.queue'));\n    }\n\n    public function handle(RunLighthouse $lighthouse, TeamService $teamService): void\n    {\n        $teamService->setTeamById($this->site->team_id);\n        $lighthouse->run($this->site, $this->batchId);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->site->id;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Livewire/Charts/LighthouseCategoriesChart.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Livewire\\Charts;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Illuminate\\Support\\Str;\nuse Livewire\\Attributes\\Isolate;\nuse Livewire\\Attributes\\Lazy;\nuse Livewire\\Attributes\\Locked;\nuse Vigilant\\Frontend\\Http\\Livewire\\BaseChart;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\n\n#[Lazy]\n#[Isolate]\nclass LighthouseCategoriesChart extends BaseChart\n{\n    #[Locked]\n    public int $lighthouseMonitorId = 0;\n\n    public int $height = 200;\n\n    public function mount(array $data): void\n    {\n        Validator::make($data, [\n            'lighthouseMonitorId' => 'required',\n        ])->validate();\n\n        $this->lighthouseMonitorId = $data['lighthouseMonitorId'];\n    }\n\n    public function data(): array\n    {\n        $results = LighthouseResult::query()\n            ->where('lighthouse_monitor_id', '=', $this->lighthouseMonitorId)\n            ->whereNull('batch_id')\n            ->get();\n\n        $labels = $results->pluck('created_at')->map(fn (Carbon $carbon): string => $carbon->toDateTimeString());\n\n        $colors = $this->getChartColors();\n\n        return [\n            'type' => 'line',\n            'data' => [\n                'labels' => $labels,\n                'datasets' => [\n                    $this->dataset([\n                        'label' => 'Performance',\n                        'data' => $results->pluck('performance')->map(fn (float $value): float => $value * 100),\n                        'borderColor' => $colors[0]['border'], // blue\n                        'backgroundColor' => $colors[0]['bg'],\n                        'fill' => true,\n                        'unit' => '%',\n                    ]),\n                    $this->dataset([\n                        'label' => 'Accessibility',\n                        'data' => $results->pluck('accessibility')->map(fn (float $value): float => $value * 100),\n                        'borderColor' => $colors[2]['border'], // green\n                        'backgroundColor' => $colors[2]['bg'],\n                        'fill' => true,\n                        'unit' => '%',\n                    ]),\n                    $this->dataset([\n                        'label' => 'Best Practices',\n                        'data' => $results->pluck('best_practices')->map(fn (float $value): float => $value * 100),\n                        'borderColor' => $colors[4]['border'], // purple\n                        'backgroundColor' => $colors[4]['bg'],\n                        'fill' => true,\n                        'unit' => '%',\n                    ]),\n                    $this->dataset([\n                        'label' => 'SEO',\n                        'data' => $results->pluck('seo')->map(fn (float $value): float => $value * 100),\n                        'borderColor' => $colors[3]['border'], // orange\n                        'backgroundColor' => $colors[3]['bg'],\n                        'fill' => true,\n                        'unit' => '%',\n                    ]),\n                ],\n            ],\n            'options' => [\n                'plugins' => [\n                    'legend' => [\n                        'display' => true,\n                        'position' => 'top',\n                        'align' => 'start',\n                    ],\n                ],\n                'scales' => [\n                    'y' => [\n                        'display' => true,\n                        'min' => 0,\n                        'max' => 100,\n                    ],\n                    'x' => [\n                        'display' => false,\n                    ],\n                ],\n            ],\n        ];\n    }\n\n    protected function getIdentifier(): string\n    {\n        return Str::slug(get_class($this)).$this->lighthouseMonitorId;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Livewire/Charts/NumericLighthouseChart.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Livewire\\Charts;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Illuminate\\Support\\Str;\nuse Livewire\\Attributes\\Isolate;\nuse Livewire\\Attributes\\Lazy;\nuse Livewire\\Attributes\\Locked;\nuse Vigilant\\Frontend\\Http\\Livewire\\BaseChart;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResultAudit;\n\n#[Lazy]\n#[Isolate]\nclass NumericLighthouseChart extends BaseChart\n{\n    #[Locked]\n    public int $lighthouseMonitorId = 0;\n\n    public int $height = 200;\n\n    #[Locked]\n    public string $audit = '';\n\n    public function mount(array $data): void\n    {\n        Validator::make($data, [\n            'lighthouseMonitorId' => 'required',\n        ])->validate();\n\n        $this->lighthouseMonitorId = $data['lighthouseMonitorId'];\n    }\n\n    public function data(): array\n    {\n        $resultIds = LighthouseResult::query()\n            ->select(['id'])\n            ->where('lighthouse_monitor_id', '=', $this->lighthouseMonitorId)\n            ->get()\n            ->pluck('id');\n\n        $audits = LighthouseResultAudit::query()\n            ->whereIn('lighthouse_result_id', $resultIds)\n            ->where('audit', '=', $this->audit)\n            ->get();\n\n        $audit = $audits->first();\n\n        if ($audit === null) {\n            return [];\n        }\n\n        $title = $audit['title'];\n        $unit = $audit['numericUnit'];\n\n        $formatter = match ($unit) {\n            'millisecond' => fn (mixed $value): float => round($value / 1000, 1),\n            default => fn (mixed $value): mixed => $value ?? '-',\n        };\n\n        $color = $this->getChartColor(0); // Use first color (blue)\n\n        return [\n            'type' => 'line',\n            'data' => [\n                'labels' => $audits->map(fn (LighthouseResultAudit $audit): string => $audit->created_at?->toDateTimeString('minute') ?? '-')->toArray(),\n                'datasets' => [\n                    $this->dataset([\n                        'label' => $title,\n                        'data' => $audits->map(fn (LighthouseResultAudit $audit): string => $formatter($audit['numericValue']))->toArray(),\n                        'borderColor' => $color['border'],\n                        'backgroundColor' => $color['bg'],\n                        'fill' => true,\n                        'unit' => 's',\n                    ]),\n                ],\n            ],\n            'options' => [\n                'plugins' => [\n                    'legend' => [\n                        'display' => false,\n                    ],\n                ],\n                'scales' => [\n                    'y' => [\n                        'display' => true,\n                        'min' => 0,\n                    ],\n                    'x' => [\n                        'display' => false,\n                    ],\n                ],\n            ],\n        ];\n    }\n\n    protected function getIdentifier(): string\n    {\n        return Str::slug(get_class($this)).$this->lighthouseMonitorId.$this->audit;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Livewire/Forms/LighthouseSiteForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Form;\nuse Vigilant\\Core\\Validation\\CanEnableRule;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\n\nclass LighthouseSiteForm extends Form\n{\n    #[Locked]\n    public ?int $site_id;\n\n    #[Validate('required|url|max:255')]\n    public string $url = '';\n\n    public bool $enabled = true;\n\n    public array $settings = [\n        'host' => '',\n    ];\n\n    public int $interval = 60 * 24;\n\n    public function getRules(): array\n    {\n        return array_merge(parent::getRules(),\n            [\n                'enabled' => ['boolean', new CanEnableRule(LighthouseMonitor::class)],\n                'interval' => ['required', 'integer', 'in:'.implode(',', array_keys(config('lighthouse.intervals')))],\n            ]\n        );\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Livewire/LighthouseSiteForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Livewire;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Frontend\\Traits\\CanBeInline;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\n\nclass LighthouseSiteForm extends Component\n{\n    use CanBeInline;\n    use DisplaysAlerts;\n\n    public Forms\\LighthouseSiteForm $form;\n\n    #[Locked]\n    public LighthouseMonitor $lighthouseMonitor;\n\n    public function mount(?LighthouseMonitor $monitor): void\n    {\n        if ($monitor !== null) {\n            if ($monitor->exists) {\n                $this->authorize('update', $monitor);\n            } else {\n                $this->authorize('create', $monitor);\n                /** @var int $defaultInterval */\n                $defaultInterval = collect(config('lighthouse.intervals'))->keys()->first() ?? 60 * 24; // @phpstan-ignore-line\n                $this->form->interval = $defaultInterval;\n            }\n\n            $this->form->fill($monitor->toArray());\n            $this->lighthouseMonitor = $monitor;\n        }\n    }\n\n    #[On('save')]\n    public function save(): void\n    {\n        $this->validate();\n\n        if ($this->lighthouseMonitor->exists) {\n            $this->authorize('update', $this->lighthouseMonitor);\n\n            $this->lighthouseMonitor->update($this->form->all());\n        } else {\n            $this->authorize('create', $this->lighthouseMonitor);\n\n            $this->lighthouseMonitor = LighthouseMonitor::query()->create(\n                $this->form->all()\n            );\n        }\n\n        if (! $this->inline) {\n            $this->alert(\n                __('Saved'),\n                __('Lighthouse monitor was successfully :action',\n                    ['action' => $this->lighthouseMonitor->wasRecentlyCreated ? 'created' : 'saved']),\n                AlertType::Success\n            );\n            $this->redirectRoute('lighthouse.index', ['monitor' => $this->lighthouseMonitor]);\n        }\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'lighthouse::livewire.lighthouse-site-form';\n\n        return view($view, [\n            'updating' => $this->lighthouseMonitor->exists,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Livewire/LighthouseSites.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Livewire;\n\nuse Livewire\\Component;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\n\nclass LighthouseSites extends Component\n{\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'lighthouse::livewire.lighthouse-sites';\n        $hasMonitors = LighthouseMonitor::query()->exists();\n\n        return view($view, [\n            'hasMonitors' => $hasMonitors,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Livewire/Monitor/Dashboard.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Livewire\\Monitor;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\nuse Vigilant\\Lighthouse\\Actions\\CalculateTimeDifference;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\n\nclass Dashboard extends Component\n{\n    #[Locked]\n    public int $monitorId;\n\n    public function mount(int $monitorId): void\n    {\n        $this->monitorId = $monitorId;\n    }\n\n    public function render(): mixed\n    {\n        /** @var CalculateTimeDifference $timeDifference */\n        $timeDifference = app(CalculateTimeDifference::class);\n\n        /** @var LighthouseMonitor $monitor */\n        $monitor = LighthouseMonitor::query()->findOrFail($this->monitorId);\n\n        $lastResults = $monitor\n            ->lighthouseResults()\n            ->take(10)\n            ->get();\n\n        /** @var view-string $view */\n        $view = 'lighthouse::livewire.monitor.dashboard';\n\n        return view($view, [\n            'lighthouseMonitor' => $monitor,\n            'lastResult' => [\n                'performance' => $lastResults->average('performance'),\n                'accessibility' => $lastResults->average('accessibility'),\n                'best_practices' => $lastResults->average('best_practices'),\n                'seo' => $lastResults->average('seo'),\n            ],\n            'difference' => [\n                '7d' => $timeDifference->calculate($monitor, now()->subDays(7)),\n                '30d' => $timeDifference->calculate($monitor, now()->subMonth()),\n                '90d' => $timeDifference->calculate($monitor, now()->subMonths(3)),\n                '180d' => $timeDifference->calculate($monitor, now()->subMonths(6)),\n            ],\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Livewire/Tables/LighthouseMonitorsTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Query\\JoinClause;\nuse Illuminate\\Support\\Enumerable;\nuse Illuminate\\Support\\Facades\\Gate;\nuse RamonRietdijk\\LivewireTables\\Actions\\Action;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse RamonRietdijk\\LivewireTables\\Enums\\Direction;\nuse RamonRietdijk\\LivewireTables\\Filters\\SelectFilter;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\Enums\\Status;\nuse Vigilant\\Frontend\\Integrations\\Table\\StatusColumn;\nuse Vigilant\\Lighthouse\\Jobs\\RunLighthouseJob;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass LighthouseMonitorsTable extends BaseTable\n{\n    protected string $model = LighthouseMonitor::class;\n\n    protected function columns(): array\n    {\n        return [\n            StatusColumn::make(__('Status'))\n                ->text(function (LighthouseMonitor $monitor): string {\n                    return $monitor->enabled ? __('Enabled') : __('Disabled');\n                })\n                ->status(function (LighthouseMonitor $monitor): Status {\n                    return $monitor->enabled ? Status::Success : Status::Danger;\n                }),\n\n            Column::make(__('URL'), 'url')\n                ->sortable()\n                ->searchable(),\n\n            Column::make(__('Performance'), 'performance')\n                ->displayUsing(fn (?float $value): string => static::scoreDisplay($value))\n                ->asHtml()\n                ->sortable(function (Builder $builder, Direction $direction): void {\n                    if ($direction === Direction::Ascending) {\n                        $builder->orderBy('lighthouse_results.performance');\n                    } else {\n                        $builder->orderByDesc('lighthouse_results.performance');\n                    }\n                }),\n\n            Column::make(__('Accessibility'), 'accessibility')\n                ->displayUsing(fn (?float $value): string => static::scoreDisplay($value))\n                ->asHtml()\n                ->sortable(function (Builder $builder, Direction $direction): void {\n                    if ($direction === Direction::Ascending) {\n                        $builder->orderBy('lighthouse_results.accessibility');\n                    } else {\n                        $builder->orderByDesc('lighthouse_results.accessibility');\n                    }\n                }),\n\n            Column::make(__('Best Practices'), 'best_practices')\n                ->displayUsing(fn (?float $value): string => static::scoreDisplay($value))\n                ->asHtml()\n                ->sortable(function (Builder $builder, Direction $direction): void {\n                    if ($direction === Direction::Ascending) {\n                        $builder->orderBy('lighthouse_results.best_practices');\n                    } else {\n                        $builder->orderByDesc('lighthouse_results.best_practices');\n                    }\n                }),\n\n            Column::make(__('SEO'), 'seo')\n                ->displayUsing(fn (?float $value): string => static::scoreDisplay($value))\n                ->asHtml()\n                ->sortable(function (Builder $builder, Direction $direction): void {\n                    if ($direction === Direction::Ascending) {\n                        $builder->orderBy('lighthouse_results.seo');\n                    } else {\n                        $builder->orderByDesc('lighthouse_results.seo');\n                    }\n                }),\n        ];\n    }\n\n    public static function scoreDisplay(?float $value): string\n    {\n        if ($value === null) {\n            return '-';\n        }\n\n        $percentage = round($value * 100);\n\n        $color = match (true) {\n            $percentage > 80 => 'text-green-light',\n            $percentage >= 60 => 'text-orange-light',\n            default => 'text-red-light'\n        };\n\n        return '<span class=\"'.$color.'\">'.$percentage.'%</span>';\n    }\n\n    protected function filters(): array\n    {\n        return [\n            SelectFilter::make(__('Site'), 'site_id')\n                ->options(\n                    Site::query()\n                        ->orderBy('url')\n                        ->pluck('url', 'id')\n                        ->toArray()\n                ),\n        ];\n    }\n\n    protected function actions(): array\n    {\n        return [\n            Action::make(__('Run Lighthouse'), function (Enumerable $models): void {\n                $models->each(fn (LighthouseMonitor $monitor) => RunLighthouseJob::dispatch($monitor));\n            }, 'run'),\n\n            Action::make(__('Enable'), function (Enumerable $models): void {\n                foreach ($models as $model) {\n                    if (! Gate::allows('create', $model)) {\n                        break;\n                    }\n                    $model->update(['enabled' => true]);\n\n                }\n            }, 'enable'),\n\n            Action::make(__('Disable'), function (Enumerable $models): void {\n                $models->each(fn (LighthouseMonitor $monitor) => $monitor->update(['enabled' => false]));\n            }, 'disable'),\n\n            Action::make(__('Delete'), function (Enumerable $models): void {\n                $models->each(fn (LighthouseMonitor $monitor): ?bool => $monitor->delete());\n            }, 'delete'),\n        ];\n    }\n\n    protected function appliedQuery(): Builder\n    {\n        return parent::appliedQuery()\n            ->leftJoin('lighthouse_results', function (JoinClause $join): void {\n                $join->on('lighthouse_monitors.id', '=', 'lighthouse_results.lighthouse_monitor_id')\n                    ->whereRaw('lighthouse_results.id IN (SELECT MAX(id) FROM lighthouse_results GROUP BY lighthouse_monitor_id)');\n            })->select([\n                'lighthouse_monitors.*',\n                'lighthouse_results.performance',\n                'lighthouse_results.accessibility',\n                'lighthouse_results.best_practices',\n                'lighthouse_results.seo',\n            ]);\n    }\n\n    protected function link(Model $model): ?string\n    {\n        return route('lighthouse.index', ['monitor' => $model]);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Livewire/Tables/LighthouseResultAuditsTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Str;\nuse Livewire\\Attributes\\Locked;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\HoverColumn;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResultAudit;\n\nclass LighthouseResultAuditsTable extends BaseTable\n{\n    protected string $model = LighthouseResultAudit::class;\n\n    #[Locked]\n    public int $resultId = 0;\n\n    public function mount(int $resultId): void\n    {\n        $this->resultId = $resultId;\n    }\n\n    protected function columns(): array\n    {\n        return [\n            Column::make(__('Audit'), 'title')\n                ->displayUsing(fn (string $audit): string => Str::inlineMarkdown($audit))\n                ->asHtml()\n                ->sortable(),\n\n            HoverColumn::make(__('Description'), 'description')\n                ->displayUsing(fn (mixed $value) => Str::markdown($value))\n                ->asHtml(),\n\n            HoverColumn::make(__('Explanation'), 'explanation')\n                ->sortable()\n                ->displayUsing(fn (mixed $value) => Str::markdown($value ?? ''))\n                ->asHtml(),\n\n            Column::make(__('Score'), 'score')\n                ->displayUsing(fn (?float $score) => $score !== null ? ($score * 100).'%' : '-')\n                ->sortable(),\n\n            Column::make(__('Value'), 'numericValue')\n                ->displayUsing(fn (?float $value) => $value !== null ? round($value, 2) : '-')\n                ->sortable(),\n\n            Column::make(__('Unit'), 'numericUnit')\n                ->displayUsing(fn (?string $value) => $value !== null ? $value : '-')\n                ->sortable(),\n        ];\n    }\n\n    protected function query(): Builder\n    {\n        return parent::query()\n            ->where('lighthouse_result_id', '=', $this->resultId);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Livewire/Tables/LighthouseResultsTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Livewire\\Attributes\\Locked;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\DateColumn;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\n\nclass LighthouseResultsTable extends BaseTable\n{\n    protected string $model = LighthouseResult::class;\n\n    public string $sortColumn = 'created_at';\n\n    public string $sortDirection = 'desc';\n\n    #[Locked]\n    public int $monitorId = 0;\n\n    public function mount(int $monitorId): void\n    {\n        $this->monitorId = $monitorId;\n    }\n\n    protected function columns(): array\n    {\n        return [\n            DateColumn::make(__('Ran At'), 'created_at')\n                ->sortable(),\n\n            Column::make(__('Performance'), 'performance')\n                ->displayUsing(fn (?float $value): string => $this->scoreDisplay($value))\n                ->asHtml()\n                ->sortable(),\n\n            Column::make(__('Accessibility'), 'accessibility')\n                ->displayUsing(fn (?float $value): string => $this->scoreDisplay($value))\n                ->asHtml()\n                ->sortable(),\n\n            Column::make(__('Best Practices'), 'best_practices')\n                ->displayUsing(fn (?float $value): string => $this->scoreDisplay($value))\n                ->asHtml()\n                ->sortable(),\n\n            Column::make(__('SEO'), 'seo')\n                ->displayUsing(fn (?float $value): string => $this->scoreDisplay($value))\n                ->asHtml()\n                ->sortable(),\n        ];\n    }\n\n    protected function scoreDisplay(?float $value): string\n    {\n        if ($value === null) {\n            return '-';\n        }\n\n        $percentage = round($value * 100);\n\n        $color = match (true) {\n            $percentage > 60 => 'text-orange-light',\n            $percentage > 80 => 'text-green-light',\n            default => 'text-red-light'\n        };\n\n        return '<span class=\"'.$color.'\">'.$percentage.'%</span>';\n    }\n\n    protected function link(Model $model): ?string\n    {\n        return route('lighthouse.result.index', ['result' => $model]);\n    }\n\n    protected function query(): Builder\n    {\n        return parent::query()\n            ->where('lighthouse_monitor_id', '=', $this->monitorId)\n            ->whereNull('batch_id');\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Models/LighthouseMonitor.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Lighthouse\\Observers\\LighthouseMonitorObserver;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Observers\\TeamObserver;\n\n/**\n * @property int $id\n * @property bool $enabled\n * @property ?int $site_id\n * @property int $team_id\n * @property ?string $batch_id\n * @property string $url\n * @property array $settings\n * @property int $interval\n * @property ?Carbon $next_run\n * @property ?Carbon $run_started_at\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Site $site\n * @property Collection<int, LighthouseResult> $lighthouseResults\n */\n#[ObservedBy([TeamObserver::class, LighthouseMonitorObserver::class])]\n#[ScopedBy([TeamScope::class])]\nclass LighthouseMonitor extends Model\n{\n    protected $guarded = [];\n\n    protected $casts = [\n        'enabled' => 'bool',\n        'settings' => 'array',\n        'next_run' => 'datetime',\n        'run_started_at' => 'datetime',\n    ];\n\n    public function site(): BelongsTo\n    {\n        return $this->belongsTo(Site::class);\n    }\n\n    public function lighthouseResults(): HasMany\n    {\n        return $this->hasMany(LighthouseResult::class);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Models/LighthouseResult.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Prunable;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Core\\Concerns\\HasDataRetention;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Users\\Observers\\TeamObserver;\n\n/**\n * @property int $id\n * @property int $lighthouse_monitor_id\n * @property int $team_id\n * @property float $performance\n * @property float $accessibility\n * @property float $best_practices\n * @property float $seo\n * @property bool $aggregated\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property LighthouseMonitor $lighthouseSite\n * @property Collection<int, LighthouseResultAudit> $audits\n */\n#[ObservedBy([TeamObserver::class])]\n#[ScopedBy([TeamScope::class])]\nclass LighthouseResult extends Model\n{\n    use HasDataRetention;\n    use Prunable;\n\n    protected $guarded = [];\n\n    protected $casts = [\n        'aggregated' => 'bool',\n    ];\n\n    public function lighthouseSite(): BelongsTo\n    {\n        return $this->belongsTo(LighthouseMonitor::class, 'lighthouse_monitor_id');\n    }\n\n    public function audits(): HasMany\n    {\n        return $this->hasMany(LighthouseResultAudit::class);\n    }\n\n    public function prunable(): Builder\n    {\n        return static::withoutGlobalScopes()->where('created_at', '<=', $this->retentionPeriod());\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Models/LighthouseResultAudit.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Users\\Observers\\TeamObserver;\n\n/**\n * @property int $id\n * @property int $lighthouse_result_id\n * @property int $team_id\n * @property string $audit\n * @property string $title\n * @property string $explanation\n * @property string $description\n * @property ?float $score\n * @property string $scoreDisplayMode\n * @property ?array $details\n * @property ?array $warnings\n * @property ?array $items\n * @property ?array $metricSavings\n * @property ?float $guidanceLevel\n * @property ?float $numericValue\n * @property ?string $numericUnit\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?LighthouseResult $lighthouseResult\n */\n#[ObservedBy([TeamObserver::class])]\n#[ScopedBy([TeamScope::class])]\nclass LighthouseResultAudit extends Model\n{\n    protected $guarded = [];\n\n    protected $casts = [\n        'details' => 'array',\n        'warnings' => 'array',\n        'items' => 'array',\n        'metricSavings' => 'array',\n    ];\n\n    public function lighthouseResult(): BelongsTo\n    {\n        return $this->belongsTo(LighthouseResult::class);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/CategoryScoreChangedNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications;\n\nuse Vigilant\\Lighthouse\\Data\\CategoryResultDifferenceData;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AverageScoreChangesCondition;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass CategoryScoreChangedNotification extends Notification implements HasSite\n{\n    public static string $name = 'Lighthouse score changed';\n\n    public Level $level = Level::Warning;\n\n    public static array $defaultConditions = [\n        'type' => 'group',\n        'children' => [\n            [\n                'type' => 'condition',\n                'condition' => AverageScoreChangesCondition::class,\n                'operator' => '>=',\n                'value' => 20,\n            ],\n        ],\n    ];\n\n    public function __construct(\n        public LighthouseResult $result,\n        public CategoryResultDifferenceData $data\n    ) {}\n\n    public function title(): string\n    {\n        return __('Average lighthouse score changed on :url', [\n            'url' => $this->result->lighthouseSite->url,\n        ]);\n    }\n\n    public function description(): string\n    {\n        $performanceOld = $this->data->performanceOld() * 100;\n        $performanceNew = $this->data->performanceNew() * 100;\n\n        $accessibilityOld = $this->data->accessibilityOld() * 100;\n        $accessibilityNew = $this->data->accessibilityNew() * 100;\n\n        $bestPracticesOld = $this->data->bestPracticesOld() * 100;\n        $bestPracticesNew = $this->data->bestPracticesNew() * 100;\n\n        $seoOld = $this->data->seoOld() * 100;\n        $seoNew = $this->data->seoNew() * 100;\n\n        return __('New values are: Performance :performance, Accessibility :accessibility, Best Practices :best_practices, SEO :seo',\n            [\n                'performance' => \"$performanceOld% => $performanceNew%\",\n                'accessibility' => \"$accessibilityOld% => $accessibilityNew%\",\n                'best_practices' => \"$bestPracticesOld% => $bestPracticesNew%\",\n                'seo' => \"$seoOld% => $seoNew%\",\n            ]\n        );\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when any Lighthouse category score changes significantly.');\n    }\n\n    public function level(): Level\n    {\n        return $this->mostlyNegative()\n            ? Level::Warning\n            : Level::Success;\n    }\n\n    protected function mostlyNegative(): bool\n    {\n        return $this->data->averageDifference() < 0;\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->result->id;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->result->lighthouseSite->site;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Audit/AuditChangesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit;\n\nuse Vigilant\\Lighthouse\\Notifications\\NumericAuditChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AuditChangesCondition extends Condition\n{\n    public static string $name = 'Numeric audit value changes by percentage';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var NumericAuditChangedNotification $notification */\n        $percentChange = abs($notification->percentChanged);\n\n        return match ($operator) {\n            '>' => $percentChange > $value,\n            '>=' => $percentChange >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Audit/AuditDecreasesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit;\n\nuse Vigilant\\Lighthouse\\Notifications\\NumericAuditChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AuditDecreasesCondition extends Condition\n{\n    public static string $name = 'Numeric audit value decreases by percentage';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var NumericAuditChangedNotification $notification */\n        $percentChange = $notification->percentChanged;\n\n        return match ($operator) {\n            '>' => $percentChange < -$value,\n            '>=' => $percentChange <= -$value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Audit/AuditIncreasesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit;\n\nuse Vigilant\\Lighthouse\\Notifications\\NumericAuditChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AuditIncreasesCondition extends Condition\n{\n    public static string $name = 'Numeric audit value increases by percentage';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var NumericAuditChangedNotification $notification */\n        $percentChange = $notification->percentChanged;\n\n        return match ($operator) {\n            '>' => $percentChange > $value,\n            '>=' => $percentChange >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Audit/AuditPercentCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit;\n\nuse Vigilant\\Lighthouse\\Notifications\\NumericAuditChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AuditPercentCondition extends Condition\n{\n    public static string $name = 'Percent change';\n\n    public function operands(): array\n    {\n        return [\n            'relative' => 'Relative',\n            'absolute' => 'Absolute',\n        ];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '<>' => 'Not equal to',\n            '<' => 'Less than',\n            '<=' => 'Less or equal than',\n            '>' => 'Greater than',\n            '>=' => 'Greater or equal than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var NumericAuditChangedNotification $notification */\n        $percent = $notification->percentChanged;\n\n        if ($operand === 'absolute') {\n            $percent = abs($percent);\n        }\n\n        return match ($operator) {\n            '=' => $percent == $value,\n            '<>' => $percent != $value,\n            '<' => $percent < $value,\n            '<=' => $percent <= $value,\n            '>' => $percent > $value,\n            '>=' => $percent >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Audit/AuditTypeCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit;\n\nuse Vigilant\\Lighthouse\\Models\\LighthouseResultAudit;\nuse Vigilant\\Lighthouse\\Notifications\\NumericAuditChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\SelectCondition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AuditTypeCondition extends SelectCondition\n{\n    public static string $name = 'Audit Type';\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '<>' => 'Not equal to',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var NumericAuditChangedNotification $notification */\n        $audit = $notification->audit->audit;\n\n        return match ($operator) {\n            '=' => $audit == $value,\n            '<>' => $audit != $value,\n            default => false,\n        };\n    }\n\n    public function options(): array\n    {\n        return cache()->remember(\n            'audit-type-condition:options',\n            now()->addDay(),\n            function (): array {\n                return LighthouseResultAudit::query()\n                    ->whereNotNull('numericValue')\n                    ->select('audit', 'title', 'numericUnit')\n                    ->distinct()\n                    ->get()\n                    ->mapWithKeys(fn (LighthouseResultAudit $audit) => [$audit->audit => $audit->title.' ('.$audit->numericUnit.')'])\n                    ->toArray();\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Audit/AuditValueCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit;\n\nuse Vigilant\\Lighthouse\\Notifications\\NumericAuditChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AuditValueCondition extends Condition\n{\n    public static string $name = 'Audit value';\n\n    public function operands(): array\n    {\n        return [\n            'new' => 'New value',\n            'old' => 'Old value',\n        ];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '<>' => 'Not equal to',\n            '<' => 'Less than',\n            '<=' => 'Less than or equal to',\n            '>' => 'Greater than',\n            '>=' => 'Greater than or equal to',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var NumericAuditChangedNotification $notification */\n        $auditValue = $operand === 'old'\n            ? $notification->previous\n            : $notification->current;\n\n        return match ($operator) {\n            '=' => $auditValue == $value,\n            '<>' => $auditValue != $value,\n            '<' => $auditValue < $value,\n            '<=' => $auditValue <= $value,\n            '>' => $auditValue > $value,\n            '>=' => $auditValue >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/AccessibilityPercentScoreCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\n\nclass AccessibilityPercentScoreCondition extends ScoreCondition\n{\n    public static string $name = 'Accessibility score change in percent';\n\n    protected function score(CategoryScoreChangedNotification $notification): float\n    {\n        return $notification->data->accessibilityDifference();\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/AccessibilityScoreDecreasesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AccessibilityScoreDecreasesCondition extends Condition\n{\n    public static string $name = 'Accessibility score decreases';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $change = $notification->data->accessibilityDifference();\n\n        return match ($operator) {\n            '>' => $change < -$value,\n            '>=' => $change <= -$value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/AccessibilityScoreIncreasesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AccessibilityScoreIncreasesCondition extends Condition\n{\n    public static string $name = 'Accessibility score increases';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $change = $notification->data->accessibilityDifference();\n\n        return match ($operator) {\n            '>' => $change > $value,\n            '>=' => $change >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/AccessibilityScoreValueCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AccessibilityScoreValueCondition extends Condition\n{\n    public static string $name = 'Accessibility score value';\n\n    public function operands(): array\n    {\n        return [\n            'new' => 'New value',\n            'old' => 'Old value',\n        ];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '<>' => 'Not equal to',\n            '<' => 'Less than',\n            '<=' => 'Less than or equal to',\n            '>' => 'Greater than',\n            '>=' => 'Greater than or equal to',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $score = $operand === 'old'\n            ? $notification->data->accessibilityOld() * 100\n            : $notification->data->accessibilityNew() * 100;\n\n        return match ($operator) {\n            '=' => $score == $value,\n            '<>' => $score != $value,\n            '<' => $score < $value,\n            '<=' => $score <= $value,\n            '>' => $score > $value,\n            '>=' => $score >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/AverageScoreChangesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AverageScoreChangesCondition extends Condition\n{\n    public static string $name = 'Average score changes';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $change = abs($notification->data->averageDifference());\n\n        return match ($operator) {\n            '>' => $change > $value,\n            '>=' => $change >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/AverageScoreCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AverageScoreCondition extends Condition\n{\n    public static string $name = 'Average Score Change in percentage';\n\n    public function operands(): array\n    {\n        return [\n            'relative' => 'Relative',\n            'absolute' => 'Absolute',\n        ];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '<>' => 'Not equal to',\n            '<' => 'Less than',\n            '<=' => 'Less or equal than',\n            '>' => 'Greater than',\n            '>=' => 'Greater or equal than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $averageScore = $notification->data->averageDifference();\n\n        if ($operand === 'absolute') {\n            $averageScore = abs($averageScore);\n        }\n\n        return match ($operator) {\n            '=' => $averageScore == $value,\n            '<>' => $averageScore != $value,\n            '<' => $averageScore < $value,\n            '<=' => $averageScore <= $value,\n            '>' => $averageScore > $value,\n            '>=' => $averageScore >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/AverageScoreDecreasesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AverageScoreDecreasesCondition extends Condition\n{\n    public static string $name = 'Average score decreases';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $change = $notification->data->averageDifference();\n\n        return match ($operator) {\n            '>' => $change < -$value,\n            '>=' => $change <= -$value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/AverageScoreIncreasesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AverageScoreIncreasesCondition extends Condition\n{\n    public static string $name = 'Average score increases';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $change = $notification->data->averageDifference();\n\n        return match ($operator) {\n            '>' => $change > $value,\n            '>=' => $change >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/AverageScoreValueCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass AverageScoreValueCondition extends Condition\n{\n    public static string $name = 'Average score value';\n\n    public function operands(): array\n    {\n        return [\n            'new' => 'New value',\n            'old' => 'Old value',\n        ];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '<>' => 'Not equal to',\n            '<' => 'Less than',\n            '<=' => 'Less than or equal to',\n            '>' => 'Greater than',\n            '>=' => 'Greater than or equal to',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $scores = $operand === 'old'\n            ? [\n                $notification->data->performanceOld(),\n                $notification->data->accessibilityOld(),\n                $notification->data->bestPracticesOld(),\n                $notification->data->seoOld(),\n            ]\n            : [\n                $notification->data->performanceNew(),\n                $notification->data->accessibilityNew(),\n                $notification->data->bestPracticesNew(),\n                $notification->data->seoNew(),\n            ];\n\n        $averageScore = collect($scores)->average() * 100;\n\n        return match ($operator) {\n            '=' => $averageScore == $value,\n            '<>' => $averageScore != $value,\n            '<' => $averageScore < $value,\n            '<=' => $averageScore <= $value,\n            '>' => $averageScore > $value,\n            '>=' => $averageScore >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/BestPracticesPercentScoreCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\n\nclass BestPracticesPercentScoreCondition extends ScoreCondition\n{\n    public static string $name = 'Best practices score change in percent';\n\n    protected function score(CategoryScoreChangedNotification $notification): float\n    {\n        return $notification->data->bestPracticesDifference();\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/BestPracticesScoreDecreasesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass BestPracticesScoreDecreasesCondition extends Condition\n{\n    public static string $name = 'Best Practices score decreases';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $change = $notification->data->bestPracticesDifference();\n\n        return match ($operator) {\n            '>' => $change < -$value,\n            '>=' => $change <= -$value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/BestPracticesScoreIncreasesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass BestPracticesScoreIncreasesCondition extends Condition\n{\n    public static string $name = 'Best Practices score increases';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $change = $notification->data->bestPracticesDifference();\n\n        return match ($operator) {\n            '>' => $change > $value,\n            '>=' => $change >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/BestPracticesScoreValueCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass BestPracticesScoreValueCondition extends Condition\n{\n    public static string $name = 'Best Practices score value';\n\n    public function operands(): array\n    {\n        return [\n            'new' => 'New value',\n            'old' => 'Old value',\n        ];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '<>' => 'Not equal to',\n            '<' => 'Less than',\n            '<=' => 'Less than or equal to',\n            '>' => 'Greater than',\n            '>=' => 'Greater than or equal to',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $score = $operand === 'old'\n            ? $notification->data->bestPracticesOld() * 100\n            : $notification->data->bestPracticesNew() * 100;\n\n        return match ($operator) {\n            '=' => $score == $value,\n            '<>' => $score != $value,\n            '<' => $score < $value,\n            '<=' => $score <= $value,\n            '>' => $score > $value,\n            '>=' => $score >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/PerformancePercentScoreCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\n\nclass PerformancePercentScoreCondition extends ScoreCondition\n{\n    public static string $name = 'Performance score change in percent';\n\n    protected function score(CategoryScoreChangedNotification $notification): float\n    {\n        return $notification->data->performanceDifference();\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/PerformanceScoreDecreasesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass PerformanceScoreDecreasesCondition extends Condition\n{\n    public static string $name = 'Performance score decreases';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $change = $notification->data->performanceDifference();\n\n        return match ($operator) {\n            '>' => $change < -$value,\n            '>=' => $change <= -$value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/PerformanceScoreIncreasesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass PerformanceScoreIncreasesCondition extends Condition\n{\n    public static string $name = 'Performance score increases';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $change = $notification->data->performanceDifference();\n\n        return match ($operator) {\n            '>' => $change > $value,\n            '>=' => $change >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/PerformanceScoreValueCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass PerformanceScoreValueCondition extends Condition\n{\n    public static string $name = 'Performance score value';\n\n    public function operands(): array\n    {\n        return [\n            'new' => 'New value',\n            'old' => 'Old value',\n        ];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '<>' => 'Not equal to',\n            '<' => 'Less than',\n            '<=' => 'Less than or equal to',\n            '>' => 'Greater than',\n            '>=' => 'Greater than or equal to',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $score = $operand === 'old'\n            ? $notification->data->performanceOld() * 100\n            : $notification->data->performanceNew() * 100;\n\n        return match ($operator) {\n            '=' => $score == $value,\n            '<>' => $score != $value,\n            '<' => $score < $value,\n            '<=' => $score <= $value,\n            '>' => $score > $value,\n            '>=' => $score >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/ScoreCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nabstract class ScoreCondition extends Condition\n{\n    public static string $name = 'Score';\n\n    public function operands(): array\n    {\n        return [\n            'relative' => 'Relative',\n            'absolute' => 'Absolute',\n        ];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '<>' => 'Not equal to',\n            '<' => 'Less than',\n            '<=' => 'Less or equal than',\n            '>' => 'Greater than',\n            '>=' => 'Greater or equal than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $score = $this->score($notification);\n\n        if ($operand === 'absolute') {\n            $score = abs($score);\n        }\n\n        return match ($operator) {\n            '=' => $score == $value,\n            '<>' => $score != $value,\n            '<' => $score < $value,\n            '<=' => $score <= $value,\n            '>' => $score > $value,\n            '>=' => $score >= $value,\n            default => false,\n        };\n    }\n\n    abstract protected function score(CategoryScoreChangedNotification $notification): float;\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/SeoPercentPercentScoreCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\n\nclass SeoPercentPercentScoreCondition extends ScoreCondition\n{\n    public static string $name = 'SEO score change in percent';\n\n    protected function score(CategoryScoreChangedNotification $notification): float\n    {\n        return $notification->data->seoDifference();\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/SeoScoreDecreasesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass SeoScoreDecreasesCondition extends Condition\n{\n    public static string $name = 'SEO score decreases';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $change = $notification->data->seoDifference();\n\n        return match ($operator) {\n            '>' => $change < -$value,\n            '>=' => $change <= -$value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/SeoScoreIncreasesCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass SeoScoreIncreasesCondition extends Condition\n{\n    public static string $name = 'SEO score increases';\n\n    public function operands(): array\n    {\n        return [];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '>=' => 'By at least',\n            '>' => 'By more than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $change = $notification->data->seoDifference();\n\n        return match ($operator) {\n            '>' => $change > $value,\n            '>=' => $change >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/Conditions/Category/SeoScoreValueCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications\\Conditions\\Category;\n\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass SeoScoreValueCondition extends Condition\n{\n    public static string $name = 'SEO score value';\n\n    public function operands(): array\n    {\n        return [\n            'new' => 'New value',\n            'old' => 'Old value',\n        ];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '<>' => 'Not equal to',\n            '<' => 'Less than',\n            '<=' => 'Less than or equal to',\n            '>' => 'Greater than',\n            '>=' => 'Greater than or equal to',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var CategoryScoreChangedNotification $notification */\n        $score = $operand === 'old'\n            ? $notification->data->seoOld() * 100\n            : $notification->data->seoNew() * 100;\n\n        return match ($operator) {\n            '=' => $score == $value,\n            '<>' => $score != $value,\n            '<' => $score < $value,\n            '<=' => $score <= $value,\n            '>' => $score > $value,\n            '>=' => $score >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Notifications/NumericAuditChangedNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Notifications;\n\nuse Vigilant\\Lighthouse\\Models\\LighthouseResultAudit;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit\\AuditChangesCondition;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass NumericAuditChangedNotification extends Notification implements HasSite\n{\n    public static string $name = 'Lighthouse numeric audit changed';\n\n    public static bool $autoCreate = false;\n\n    public Level $level = Level::Info;\n\n    public static array $defaultConditions = [\n        'type' => 'group',\n        'children' => [\n            [\n                'type' => 'condition',\n                'condition' => AuditChangesCondition::class,\n                'operator' => '>=',\n                'value' => 10,\n            ],\n        ],\n    ];\n\n    public function __construct(\n        public LighthouseResultAudit $audit,\n        public float $percentChanged,\n        public float $previous,\n        public float $current,\n    ) {}\n\n    public function title(): string\n    {\n        return __('Lighthouse numeric audit \\':audit\\' on :url changed by :percent %', [\n            'audit' => $this->audit->title,\n            'url' => $this->audit->lighthouseResult->lighthouseSite->url ?? '?',\n            'percent' => round($this->percentChanged),\n        ]);\n    }\n\n    public function description(): string\n    {\n        return __('Raw value changed from from :previous to :current', [\n            'previous' => $this->roundRawValue($this->previous),\n            'current' => $this->roundRawValue($this->current),\n        ]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when a specific Lighthouse audit metric changes.');\n    }\n\n    public function site(): ?Site\n    {\n        return $this->audit->lighthouseResult?->lighthouseSite?->site;\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->audit->id;\n    }\n\n    protected function roundRawValue(float $rawValue): float\n    {\n        if ($rawValue > 10) {\n            return round($rawValue);\n        }\n\n        if ($rawValue > 0) {\n            return round($rawValue, 2);\n        }\n\n        if ($rawValue == 0) {\n            return 0.00;\n        }\n\n        $strNumber = rtrim(rtrim(sprintf('%.10f', $rawValue), '0'), '.');\n\n        return (float) $strNumber;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/Observers/LighthouseMonitorObserver.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Observers;\n\nuse Vigilant\\Lighthouse\\Jobs\\RunLighthouseJob;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\n\nclass LighthouseMonitorObserver\n{\n    public function created(LighthouseMonitor $monitor): void\n    {\n        RunLighthouseJob::dispatch($monitor);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse;\n\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\nuse Livewire\\Livewire;\nuse Vigilant\\Core\\Facades\\Navigation;\nuse Vigilant\\Core\\Policies\\AllowAllPolicy;\nuse Vigilant\\Lighthouse\\Commands\\AggregateLighthouseBatchCommand;\nuse Vigilant\\Lighthouse\\Commands\\AggregateLighthouseResultsCommand;\nuse Vigilant\\Lighthouse\\Commands\\CheckLighthouseCommand;\nuse Vigilant\\Lighthouse\\Commands\\LighthouseCommand;\nuse Vigilant\\Lighthouse\\Commands\\ScheduleLighthouseCommand;\nuse Vigilant\\Lighthouse\\Livewire\\Charts\\LighthouseCategoriesChart;\nuse Vigilant\\Lighthouse\\Livewire\\Charts\\NumericLighthouseChart;\nuse Vigilant\\Lighthouse\\Livewire\\LighthouseSiteForm;\nuse Vigilant\\Lighthouse\\Livewire\\LighthouseSites;\nuse Vigilant\\Lighthouse\\Livewire\\Monitor\\Dashboard;\nuse Vigilant\\Lighthouse\\Livewire\\Tables\\LighthouseMonitorsTable;\nuse Vigilant\\Lighthouse\\Livewire\\Tables\\LighthouseResultAuditsTable;\nuse Vigilant\\Lighthouse\\Livewire\\Tables\\LighthouseResultsTable;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit\\AuditChangesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit\\AuditDecreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit\\AuditIncreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit\\AuditPercentCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit\\AuditTypeCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit\\AuditValueCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AccessibilityPercentScoreCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AccessibilityScoreDecreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AccessibilityScoreIncreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AccessibilityScoreValueCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AverageScoreChangesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AverageScoreCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AverageScoreDecreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AverageScoreIncreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AverageScoreValueCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\BestPracticesPercentScoreCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\BestPracticesScoreDecreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\BestPracticesScoreIncreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\BestPracticesScoreValueCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\PerformancePercentScoreCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\PerformanceScoreDecreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\PerformanceScoreIncreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\PerformanceScoreValueCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\SeoPercentPercentScoreCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\SeoScoreDecreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\SeoScoreIncreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\SeoScoreValueCondition;\nuse Vigilant\\Lighthouse\\Notifications\\NumericAuditChangedNotification;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Sites\\Conditions\\SiteCondition;\nuse Vigilant\\Users\\Models\\User;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function register(): void\n    {\n        $this\n            ->registerConfig();\n    }\n\n    protected function registerConfig(): static\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/lighthouse.php', 'lighthouse');\n\n        return $this;\n    }\n\n    public function boot(): void\n    {\n        $this\n            ->bootConfig()\n            ->bootMigrations()\n            ->bootCommands()\n            ->bootViews()\n            ->bootLivewire()\n            ->bootRoutes()\n            ->bootNavigation()\n            ->bootNotifications()\n            ->bootGates()\n            ->bootPolicies();\n    }\n\n    protected function bootConfig(): static\n    {\n        $this->publishes([\n            __DIR__.'/../config/lighthouse.php' => config_path('lighthouse.php'),\n        ], 'config');\n\n        return $this;\n    }\n\n    protected function bootMigrations(): static\n    {\n        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');\n\n        return $this;\n    }\n\n    protected function bootCommands(): static\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n                LighthouseCommand::class,\n                ScheduleLighthouseCommand::class,\n                CheckLighthouseCommand::class,\n                AggregateLighthouseResultsCommand::class,\n                AggregateLighthouseBatchCommand::class,\n            ]);\n        }\n\n        return $this;\n    }\n\n    protected function bootViews(): static\n    {\n        $this->loadViewsFrom(__DIR__.'/../resources/views', 'lighthouse');\n\n        return $this;\n    }\n\n    protected function bootLivewire(): static\n    {\n        Livewire::component('lighthouse', LighthouseSites::class);\n        Livewire::component('lighthouse-sites-table', LighthouseMonitorsTable::class);\n        Livewire::component('lighthouse-results-table', LighthouseResultsTable::class);\n        Livewire::component('lighthouse-site-form', LighthouseSiteForm::class);\n        Livewire::component('lighthouse-categories-chart', LighthouseCategoriesChart::class);\n        Livewire::component('lighthouse-result-audits-table', LighthouseResultAuditsTable::class);\n\n        Livewire::component('lighthouse-numeric-chart', NumericLighthouseChart::class);\n        Livewire::component('lighthouse-monitor-dashboard', Dashboard::class);\n\n        return $this;\n    }\n\n    protected function bootRoutes(): static\n    {\n        if (! $this->app->routesAreCached()) {\n            Route::middleware(['web', 'auth'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/web.php'));\n\n            Route::prefix('api/v1/lighthouse')\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/api.php'));\n        }\n\n        return $this;\n    }\n\n    protected function bootNavigation(): static\n    {\n        Navigation::path(__DIR__.'/../resources/navigation.php');\n\n        return $this;\n    }\n\n    protected function bootNotifications(): static\n    {\n        NotificationRegistry::registerNotification([\n            CategoryScoreChangedNotification::class,\n            NumericAuditChangedNotification::class,\n        ]);\n\n        NotificationRegistry::registerCondition(CategoryScoreChangedNotification::class, [\n            SiteCondition::class,\n            // Change-based conditions (new, clearer)\n            AverageScoreIncreasesCondition::class,\n            AverageScoreDecreasesCondition::class,\n            AverageScoreChangesCondition::class,\n            PerformanceScoreIncreasesCondition::class,\n            PerformanceScoreDecreasesCondition::class,\n            AccessibilityScoreIncreasesCondition::class,\n            AccessibilityScoreDecreasesCondition::class,\n            BestPracticesScoreIncreasesCondition::class,\n            BestPracticesScoreDecreasesCondition::class,\n            SeoScoreIncreasesCondition::class,\n            SeoScoreDecreasesCondition::class,\n            // Absolute value conditions\n            AverageScoreValueCondition::class,\n            PerformanceScoreValueCondition::class,\n            AccessibilityScoreValueCondition::class,\n            BestPracticesScoreValueCondition::class,\n            SeoScoreValueCondition::class,\n            // Legacy conditions (kept for backward compatibility)\n            AverageScoreCondition::class,\n            AccessibilityPercentScoreCondition::class,\n            BestPracticesPercentScoreCondition::class,\n            PerformancePercentScoreCondition::class,\n            SeoPercentPercentScoreCondition::class,\n        ]);\n\n        NotificationRegistry::registerCondition(NumericAuditChangedNotification::class, [\n            // Change-based conditions (new, clearer)\n            AuditIncreasesCondition::class,\n            AuditDecreasesCondition::class,\n            AuditChangesCondition::class,\n            // Absolute value condition\n            AuditValueCondition::class,\n            // Legacy condition (kept for backward compatibility)\n            AuditPercentCondition::class,\n            AuditTypeCondition::class,\n        ]);\n\n        return $this;\n    }\n\n    protected function bootGates(): static\n    {\n        if (ce()) {\n            Gate::define('use-lighthouse', function (User $user): bool {\n                return ce();\n            });\n        }\n\n        return $this;\n    }\n\n    protected function bootPolicies(): static\n    {\n        if (ce()) {\n            Gate::policy(LighthouseMonitor::class, AllowAllPolicy::class);\n        }\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/testbench.yaml",
    "content": "providers:\n  - Vigilant\\Lighthouse\\ServiceProvider\n"
  },
  {
    "path": "packages/lighthouse/tests/Actions/AggregateResultsTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Tests\\Actions;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Lighthouse\\Actions\\AggregateResults;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Tests\\TestCase;\n\nclass AggregateResultsTest extends TestCase\n{\n    #[Test]\n    public function it_aggregates_result(): void\n    {\n        /** @var LighthouseMonitor $site */\n        $site = LighthouseMonitor::query()->create([\n            'team_id' => 1,\n            'url' => 'https://govigilant.io',\n            'settings' => [],\n            'interval' => '0 * * * *',\n        ]);\n\n        for ($i = 0; $i < 5; $i++) {\n            $site->lighthouseResults()->create([\n                'performance' => 1,\n                'accessibility' => 1,\n                'best_practices' => 1,\n                'seo' => 1,\n                'created_at' => now()->subHours($i),\n                'updated_at' => now()->subHours($i),\n            ]);\n        }\n\n        for ($i = 5; $i < 10; $i++) {\n            $site->lighthouseResults()->create([\n                'performance' => 0.5,\n                'accessibility' => 0.5,\n                'best_practices' => 0.5,\n                'seo' => 0.5,\n                'created_at' => now()->subHours($i),\n                'updated_at' => now()->subHours($i),\n            ]);\n        }\n\n        /** @var AggregateResults $action */\n        $action = app(AggregateResults::class);\n\n        $action->aggregate($site, now()->subHours(10), now());\n\n        $this->assertCount(1, $site->lighthouseResults);\n\n        $this->assertNotNull($site->lighthouseResults->first());\n        $this->assertNotNull($site->lighthouseResults->first());\n        $this->assertNotNull($site->lighthouseResults->first());\n        $this->assertNotNull($site->lighthouseResults->first());\n\n        $this->assertEquals(0.75, $site->lighthouseResults->first()->performance);\n        $this->assertEquals(0.75, $site->lighthouseResults->first()->accessibility);\n        $this->assertEquals(0.75, $site->lighthouseResults->first()->best_practices);\n        $this->assertEquals(0.75, $site->lighthouseResults->first()->seo);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/tests/Actions/CalculateTimeDifferenceTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Tests\\Actions;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Lighthouse\\Actions\\CalculateTimeDifference;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Tests\\TestCase;\n\nclass CalculateTimeDifferenceTest extends TestCase\n{\n    #[Test]\n    public function it_calculates_difference(): void\n    {\n        /** @var LighthouseMonitor $site */\n        $site = LighthouseMonitor::query()->create([\n            'team_id' => 1,\n            'url' => 'https://govigilant.io',\n            'settings' => [],\n            'interval' => '0 * * * *',\n        ]);\n\n        $site->lighthouseResults()->create([\n            'performance' => 1,\n            'accessibility' => 1,\n            'best_practices' => 1,\n            'seo' => 1,\n            'created_at' => now()->subHours(24),\n            'updated_at' => now()->subHours(24),\n        ]);\n\n        $site->lighthouseResults()->create([\n            'performance' => 1,\n            'accessibility' => 1,\n            'best_practices' => 1,\n            'seo' => 1,\n            'created_at' => now()->subHours(23),\n            'updated_at' => now()->subHours(23),\n        ]);\n\n        $site->lighthouseResults()->create([\n            'performance' => 1,\n            'accessibility' => 1,\n            'best_practices' => 1,\n            'seo' => 1,\n            'created_at' => now()->subHours(22),\n            'updated_at' => now()->subHours(22),\n        ]);\n\n        $site->lighthouseResults()->create([\n            'performance' => 1,\n            'accessibility' => 1,\n            'best_practices' => 1,\n            'seo' => 1,\n            'created_at' => now()->subHours(21),\n            'updated_at' => now()->subHours(21),\n        ]);\n\n        $site->lighthouseResults()->create([\n            'performance' => 1,\n            'accessibility' => 1,\n            'best_practices' => 1,\n            'seo' => 1,\n            'created_at' => now()->subHours(20),\n            'updated_at' => now()->subHours(20),\n        ]);\n\n        $site->lighthouseResults()->create([\n            'performance' => 1,\n            'accessibility' => 1,\n            'best_practices' => 1,\n            'seo' => 1,\n            'created_at' => now()->subHours(19),\n            'updated_at' => now()->subHours(19),\n        ]);\n\n        $site->lighthouseResults()->create([\n            'performance' => 1,\n            'accessibility' => 1,\n            'best_practices' => 1,\n            'seo' => 1,\n            'created_at' => now()->subHours(18),\n            'updated_at' => now()->subHours(18),\n        ]);\n\n        $site->lighthouseResults()->create([\n            'performance' => 1,\n            'accessibility' => 1,\n            'best_practices' => 1,\n            'seo' => 1,\n            'created_at' => now()->subHours(17),\n            'updated_at' => now()->subHours(17),\n        ]);\n\n        $site->lighthouseResults()->create([\n            'performance' => 1,\n            'accessibility' => 1,\n            'best_practices' => 1,\n            'seo' => 1,\n            'created_at' => now()->subHours(16),\n            'updated_at' => now()->subHours(16),\n        ]);\n\n        $site->lighthouseResults()->create([\n            'performance' => 0.5,\n            'accessibility' => 0.5,\n            'best_practices' => 0.5,\n            'seo' => 0.5,\n            'created_at' => now()->subHours(15),\n            'updated_at' => now()->subHours(15),\n        ]);\n\n        $site->lighthouseResults()->create([\n            'performance' => 0.5,\n            'accessibility' => 0.5,\n            'best_practices' => 0.5,\n            'seo' => 0.5,\n            'created_at' => now()->subHours(14),\n            'updated_at' => now()->subHours(14),\n        ]);\n\n        /** @var CalculateTimeDifference $action */\n        $action = app(CalculateTimeDifference::class);\n        $result = $action->calculate($site, now()->subHours(24));\n\n        $this->assertNotNull($result);\n        $this->assertEquals(0.5, $result->performanceNew());\n        $this->assertEquals(1, $result->performanceOld());\n        $this->assertEquals(-50, $result->performanceDifference());\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/tests/Actions/CheckLighthouseResultAuditTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Tests\\Actions;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Lighthouse\\Actions\\CheckLighthouseResultAudit;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResultAudit;\nuse Vigilant\\Lighthouse\\Notifications\\NumericAuditChangedNotification;\nuse Vigilant\\Lighthouse\\Tests\\TestCase;\n\nclass CheckLighthouseResultAuditTest extends TestCase\n{\n    #[Test]\n    public function it_dispatches_notification(): void\n    {\n        NumericAuditChangedNotification::fake();\n\n        /** @var LighthouseMonitor $site */\n        $site = LighthouseMonitor::query()->create([\n            'team_id' => 1,\n            'url' => 'https://govigilant.io',\n            'settings' => [],\n            'interval' => '0 * * * *',\n        ]);\n\n        /** @var LighthouseResult $result */\n        $result = $site->lighthouseResults()->create([\n            'performance' => 0.5,\n            'accessibility' => 0.5,\n            'best_practices' => 0.5,\n            'seo' => 0.5,\n            'created_at' => now(),\n            'updated_at' => now(),\n        ]);\n\n        for ($i = 0; $i < 100; $i++) {\n            $result->audits()->create([\n                'audit' => 'test',\n                'title' => 'test',\n                'explanation' => 'test',\n                'description' => 'test',\n                'score' => 1,\n                'scoreDisplayMode' => 'numeric',\n                'numericValue' => $i * 10,\n            ]);\n        }\n\n        /** @var LighthouseResultAudit $audit */\n        $audit = $result->audits()->create([\n            'audit' => 'test',\n            'title' => 'test',\n            'explanation' => 'test',\n            'description' => 'test',\n            'score' => 1,\n            'scoreDisplayMode' => 'numeric',\n            'numericValue' => 100,\n        ]);\n\n        /** @var CheckLighthouseResultAudit $action */\n        $action = app(CheckLighthouseResultAudit::class);\n\n        $action->check($audit);\n\n        $this->assertTrue(NumericAuditChangedNotification::wasDispatched(function (\n            NumericAuditChangedNotification $notification\n        ): bool {\n            return round($notification->percentChanged) == 15;\n        }));\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/tests/Actions/CheckLighthouseResultTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Tests\\Actions;\n\nuse Mockery\\MockInterface;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Lighthouse\\Actions\\CheckLighthouseResult;\nuse Vigilant\\Lighthouse\\Actions\\CheckLighthouseResultAudit;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Lighthouse\\Tests\\TestCase;\n\nclass CheckLighthouseResultTest extends TestCase\n{\n    #[Test]\n    public function it_dispatches_notification(): void\n    {\n        CategoryScoreChangedNotification::fake();\n\n        $this->mock(CheckLighthouseResultAudit::class, function (MockInterface $mock): void {\n            $mock->shouldReceive('check')->andReturn();\n        });\n\n        /** @var LighthouseMonitor $site */\n        $site = LighthouseMonitor::query()->create([\n            'team_id' => 1,\n            'url' => 'https://govigilant.io',\n            'settings' => [],\n            'interval' => '0 * * * *',\n        ]);\n\n        for ($i = 0; $i < 4; $i++) {\n            $site->lighthouseResults()->create([\n                'performance' => 0.5,\n                'accessibility' => 0.5,\n                'best_practices' => 0.5,\n                'seo' => 0.5,\n                'created_at' => now(),\n                'updated_at' => now(),\n            ]);\n        }\n\n        for ($i = 0; $i < 12; $i++) {\n            $site->lighthouseResults()->create([\n                'performance' => 0.7,\n                'accessibility' => 0.7,\n                'best_practices' => 0.7,\n                'seo' => 0.5,\n                'created_at' => now(),\n                'updated_at' => now(),\n            ]);\n        }\n\n        /** @var LighthouseResult $lastResult */\n        $lastResult = $site->lighthouseResults->last();\n\n        /** @var CheckLighthouseResult $action */\n        $action = app(CheckLighthouseResult::class);\n\n        $action->check($lastResult);\n\n        $this->assertTrue(CategoryScoreChangedNotification::wasDispatched());\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/tests/Actions/RunLighthouseTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Tests\\Actions;\n\nuse Illuminate\\Http\\Client\\Request;\nuse Illuminate\\Support\\Facades\\Http;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Lighthouse\\Actions\\RunLighthouse;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Tests\\TestCase;\n\nclass RunLighthouseTest extends TestCase\n{\n    #[Test]\n    public function it_runs_lighthouse(): void\n    {\n        config()->set('lighthouse.workers', ['worker']);\n        config()->set('lighthouse.lighthouse_app_url', 'app');\n\n        Http::fake([\n            'worker/lighthouse' => Http::response([], 200),\n        ])->preventStrayRequests();\n\n        $monitor = LighthouseMonitor::query()->create([\n            'enabled' => true,\n            'team_id' => 1,\n            'url' => 'vigilant',\n            'settings' => [],\n            'interval' => 60,\n        ]);\n\n        $action = app(RunLighthouse::class);\n\n        $action->run($monitor, null);\n\n        $monitor->refresh();\n        $this->assertNotNull($monitor->next_run);\n        $this->assertNotNull($monitor->run_started_at);\n\n        Http::assertSent(function (Request $request) {\n            return $request->url() === 'worker/lighthouse' &&\n                $request['website'] === 'vigilant';\n        });\n    }\n\n    #[Test]\n    public function it_gets_worker(): void\n    {\n        config()->set('lighthouse.workers', [\n            'worker_1',\n            'worker_2',\n            'worker_3',\n        ]);\n\n        $action = app(RunLighthouse::class);\n\n        $this->assertEquals('worker_1', $action->getAvailableWorker());\n        $this->assertEquals('worker_2', $action->getAvailableWorker());\n        $this->assertEquals('worker_3', $action->getAvailableWorker());\n        for ($i = 0; $i < 10; $i++) {\n            $this->assertNull($action->getAvailableWorker());\n        }\n\n        cache()->forget('lighthouse:worker:worker_1');\n        $this->assertEquals('worker_1', $action->getAvailableWorker());\n\n        cache()->flush();\n        $this->assertEquals('worker_1', $action->getAvailableWorker());\n        $this->assertEquals('worker_2', $action->getAvailableWorker());\n        $this->assertEquals('worker_3', $action->getAvailableWorker());\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/tests/Data/CategoryResultDifferenceDataTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Tests\\Data;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Lighthouse\\Data\\CategoryResultDifferenceData;\nuse Vigilant\\Lighthouse\\Tests\\TestCase;\n\nclass CategoryResultDifferenceDataTest extends TestCase\n{\n    #[Test]\n    public function test_calculations(): void\n    {\n        $data = CategoryResultDifferenceData::of([\n            'performance_old' => 0.5,\n            'performance_new' => 1,\n            'accessibility_old' => 1,\n            'accessibility_new' => 0.5,\n            'best_practices_old' => 1,\n            'best_practices_new' => 1,\n            'seo_old' => 0,\n            'seo_new' => 0,\n        ]);\n\n        $this->assertEquals(100, $data->performanceDifference());\n        $this->assertEquals(-50, $data->accessibilityDifference());\n        $this->assertEquals(0, $data->bestPracticesDifference());\n        $this->assertEquals(0, $data->seoDifference());\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/tests/Notifications/Conditions/Audit/AuditChangeConditionsTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Tests\\Notifications\\Conditions\\Audit;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResultAudit;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit\\AuditChangesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit\\AuditDecreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit\\AuditIncreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\NumericAuditChangedNotification;\nuse Vigilant\\Lighthouse\\Tests\\TestCase;\n\nclass AuditChangeConditionsTest extends TestCase\n{\n    #[Test]\n    public function it_checks_audit_increases(): void\n    {\n        $condition = new AuditIncreasesCondition;\n\n        // 50% increase\n        $notification = $this->makeNotification(100, 150, 50);\n\n        $this->assertTrue($condition->applies($notification, null, '>=', 40, null));\n        $this->assertTrue($condition->applies($notification, null, '>=', 50, null));\n        $this->assertFalse($condition->applies($notification, null, '>=', 60, null));\n        $this->assertTrue($condition->applies($notification, null, '>', 40, null));\n        $this->assertFalse($condition->applies($notification, null, '>', 50, null));\n    }\n\n    #[Test]\n    public function it_checks_audit_decreases(): void\n    {\n        $condition = new AuditDecreasesCondition;\n\n        // 33.33% decrease\n        $notification = $this->makeNotification(150, 100, -33.33);\n\n        $this->assertTrue($condition->applies($notification, null, '>=', 30, null));\n        $this->assertTrue($condition->applies($notification, null, '>=', 33, null));\n        $this->assertFalse($condition->applies($notification, null, '>=', 40, null));\n        $this->assertTrue($condition->applies($notification, null, '>', 30, null));\n        $this->assertFalse($condition->applies($notification, null, '>', 33.33, null));\n    }\n\n    #[Test]\n    public function it_checks_audit_changes_either_direction(): void\n    {\n        $conditionIncrease = new AuditChangesCondition;\n        $conditionDecrease = new AuditChangesCondition;\n\n        $notificationIncrease = $this->makeNotification(100, 150, 50);\n        $notificationDecrease = $this->makeNotification(150, 100, -33.33);\n\n        $this->assertTrue($conditionIncrease->applies($notificationIncrease, null, '>=', 40, null));\n        $this->assertTrue($conditionDecrease->applies($notificationDecrease, null, '>=', 30, null));\n        \n        $this->assertTrue($conditionIncrease->applies($notificationIncrease, null, '>', 40, null));\n        $this->assertFalse($conditionIncrease->applies($notificationIncrease, null, '>', 50, null));\n    }\n\n    #[Test]\n    public function it_does_not_trigger_increase_on_decrease(): void\n    {\n        $condition = new AuditIncreasesCondition;\n        $notification = $this->makeNotification(150, 100, -33.33);\n\n        $this->assertFalse($condition->applies($notification, null, '>=', 10, null));\n    }\n\n    #[Test]\n    public function it_does_not_trigger_decrease_on_increase(): void\n    {\n        $condition = new AuditDecreasesCondition;\n        $notification = $this->makeNotification(100, 150, 50);\n\n        $this->assertFalse($condition->applies($notification, null, '>=', 10, null));\n    }\n\n    protected function makeNotification(float $previous, float $current, float $percentChanged): NumericAuditChangedNotification\n    {\n        $monitor = LighthouseMonitor::query()->create([\n            'team_id' => 1,\n            'url' => 'https://example.com',\n            'settings' => [],\n            'interval' => '0 * * * *',\n        ]);\n\n        $result = LighthouseResult::query()->create([\n            'lighthouse_monitor_id' => $monitor->id,\n            'performance' => 0.7,\n            'accessibility' => 0.7,\n            'best_practices' => 0.7,\n            'seo' => 0.7,\n        ]);\n\n        $audit = LighthouseResultAudit::query()->create([\n            'lighthouse_result_id' => $result->id,\n            'team_id' => 1,\n            'audit' => 'first-contentful-paint',\n            'title' => 'First Contentful Paint',\n            'description' => 'Test audit',\n            'score' => 0.5,\n            'scoreDisplayMode' => 'numeric',\n            'numericValue' => $current,\n            'numericUnit' => 'millisecond',\n        ]);\n\n        return new NumericAuditChangedNotification($audit, $percentChanged, $previous, $current);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/tests/Notifications/Conditions/Audit/AuditValueConditionTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Tests\\Notifications\\Conditions\\Audit;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResultAudit;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Audit\\AuditValueCondition;\nuse Vigilant\\Lighthouse\\Notifications\\NumericAuditChangedNotification;\nuse Vigilant\\Lighthouse\\Tests\\TestCase;\n\nclass AuditValueConditionTest extends TestCase\n{\n    #[Test]\n    public function it_checks_new_value_greater_than_threshold(): void\n    {\n        $condition = new AuditValueCondition;\n\n        $notification = $this->makeNotification(100, 150, 50);\n\n        $this->assertTrue($condition->applies($notification, 'new', '>', 140, null));\n        $this->assertFalse($condition->applies($notification, 'new', '>', 150, null));\n        $this->assertTrue($condition->applies($notification, 'new', '>=', 150, null));\n    }\n\n    #[Test]\n    public function it_checks_new_value_less_than_threshold(): void\n    {\n        $condition = new AuditValueCondition;\n\n        $notification = $this->makeNotification(150, 100, -33.33);\n\n        $this->assertTrue($condition->applies($notification, 'new', '<', 110, null));\n        $this->assertFalse($condition->applies($notification, 'new', '<', 100, null));\n        $this->assertTrue($condition->applies($notification, 'new', '<=', 100, null));\n    }\n\n    #[Test]\n    public function it_checks_old_value_greater_than_threshold(): void\n    {\n        $condition = new AuditValueCondition;\n\n        $notification = $this->makeNotification(150, 100, -33.33);\n\n        $this->assertTrue($condition->applies($notification, 'old', '>', 140, null));\n        $this->assertFalse($condition->applies($notification, 'old', '>', 150, null));\n        $this->assertTrue($condition->applies($notification, 'old', '>=', 150, null));\n    }\n\n    #[Test]\n    public function it_checks_old_value_less_than_threshold(): void\n    {\n        $condition = new AuditValueCondition;\n\n        $notification = $this->makeNotification(100, 150, 50);\n\n        $this->assertTrue($condition->applies($notification, 'old', '<', 110, null));\n        $this->assertFalse($condition->applies($notification, 'old', '<', 100, null));\n        $this->assertTrue($condition->applies($notification, 'old', '<=', 100, null));\n    }\n\n    #[Test]\n    public function it_checks_equality(): void\n    {\n        $condition = new AuditValueCondition;\n\n        $notification = $this->makeNotification(100, 100, 0);\n\n        $this->assertTrue($condition->applies($notification, 'new', '=', 100, null));\n        $this->assertFalse($condition->applies($notification, 'new', '=', 150, null));\n        $this->assertTrue($condition->applies($notification, 'new', '<>', 150, null));\n    }\n\n    protected function makeNotification(float $previous, float $current, float $percentChanged): NumericAuditChangedNotification\n    {\n        $monitor = LighthouseMonitor::query()->create([\n            'team_id' => 1,\n            'url' => 'https://example.com',\n            'settings' => [],\n            'interval' => '0 * * * *',\n        ]);\n\n        $result = LighthouseResult::query()->create([\n            'lighthouse_monitor_id' => $monitor->id,\n            'performance' => 0.7,\n            'accessibility' => 0.7,\n            'best_practices' => 0.7,\n            'seo' => 0.7,\n        ]);\n\n        $audit = LighthouseResultAudit::query()->create([\n            'lighthouse_result_id' => $result->id,\n            'team_id' => 1,\n            'audit' => 'first-contentful-paint',\n            'title' => 'First Contentful Paint',\n            'description' => 'Test audit',\n            'score' => 0.5,\n            'scoreDisplayMode' => 'numeric',\n            'numericValue' => $current,\n            'numericUnit' => 'millisecond',\n        ]);\n\n        return new NumericAuditChangedNotification($audit, $percentChanged, $previous, $current);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/tests/Notifications/Conditions/Category/AverageScoreChangeConditionsTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Tests\\Notifications\\Conditions\\Category;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Lighthouse\\Data\\CategoryResultDifferenceData;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AverageScoreChangesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AverageScoreDecreasesCondition;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AverageScoreIncreasesCondition;\nuse Vigilant\\Lighthouse\\Tests\\TestCase;\n\nclass AverageScoreChangeConditionsTest extends TestCase\n{\n    #[Test]\n    public function it_checks_score_increases(): void\n    {\n        $condition = new AverageScoreIncreasesCondition;\n\n        // 20% increase\n        $notification = $this->makeNotification(0.5, 0.6);\n\n        $this->assertTrue($condition->applies($notification, null, '>=', 15, null));\n        $this->assertTrue($condition->applies($notification, null, '>=', 20, null));\n        $this->assertFalse($condition->applies($notification, null, '>=', 25, null));\n        $this->assertTrue($condition->applies($notification, null, '>', 15, null));\n        $this->assertFalse($condition->applies($notification, null, '>', 20, null));\n    }\n\n    #[Test]\n    public function it_checks_score_decreases(): void\n    {\n        $condition = new AverageScoreDecreasesCondition;\n\n        // 20% decrease\n        $notification = $this->makeNotification(0.6, 0.5);\n\n        $this->assertTrue($condition->applies($notification, null, '>=', 15, null));\n        $this->assertTrue($condition->applies($notification, null, '>=', 16.7, null));\n        $this->assertFalse($condition->applies($notification, null, '>=', 20, null));\n        $this->assertTrue($condition->applies($notification, null, '>', 15, null));\n        $this->assertFalse($condition->applies($notification, null, '>', 16.7, null));\n    }\n\n    #[Test]\n    public function it_checks_score_changes_either_direction(): void\n    {\n        $conditionIncrease = new AverageScoreChangesCondition;\n        $conditionDecrease = new AverageScoreChangesCondition;\n\n        $notificationIncrease = $this->makeNotification(0.5, 0.6);\n        $notificationDecrease = $this->makeNotification(0.6, 0.5);\n\n        $this->assertTrue($conditionIncrease->applies($notificationIncrease, null, '>=', 15, null));\n        $this->assertTrue($conditionDecrease->applies($notificationDecrease, null, '>=', 15, null));\n        \n        $this->assertTrue($conditionIncrease->applies($notificationIncrease, null, '>', 15, null));\n        $this->assertFalse($conditionIncrease->applies($notificationIncrease, null, '>', 20, null));\n    }\n\n    #[Test]\n    public function it_does_not_trigger_increase_on_decrease(): void\n    {\n        $condition = new AverageScoreIncreasesCondition;\n        $notification = $this->makeNotification(0.6, 0.5);\n\n        $this->assertFalse($condition->applies($notification, null, '>=', 10, null));\n    }\n\n    #[Test]\n    public function it_does_not_trigger_decrease_on_increase(): void\n    {\n        $condition = new AverageScoreDecreasesCondition;\n        $notification = $this->makeNotification(0.5, 0.6);\n\n        $this->assertFalse($condition->applies($notification, null, '>=', 10, null));\n    }\n\n    protected function makeNotification(float $oldScore, float $newScore): CategoryScoreChangedNotification\n    {\n        $monitor = LighthouseMonitor::query()->create([\n            'team_id' => 1,\n            'url' => 'https://example.com',\n            'settings' => [],\n            'interval' => '0 * * * *',\n        ]);\n\n        $result = LighthouseResult::query()->create([\n            'lighthouse_monitor_id' => $monitor->id,\n            'performance' => $newScore,\n            'accessibility' => $newScore,\n            'best_practices' => $newScore,\n            'seo' => $newScore,\n        ]);\n\n        $data = CategoryResultDifferenceData::of([\n            'performance_old' => $oldScore,\n            'performance_new' => $newScore,\n            'accessibility_old' => $oldScore,\n            'accessibility_new' => $newScore,\n            'best_practices_old' => $oldScore,\n            'best_practices_new' => $newScore,\n            'seo_old' => $oldScore,\n            'seo_new' => $newScore,\n        ]);\n\n        return new CategoryScoreChangedNotification($result, $data);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/tests/Notifications/Conditions/Category/AverageScoreValueConditionTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Tests\\Notifications\\Conditions\\Category;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Lighthouse\\Data\\CategoryResultDifferenceData;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AverageScoreValueCondition;\nuse Vigilant\\Lighthouse\\Tests\\TestCase;\n\nclass AverageScoreValueConditionTest extends TestCase\n{\n    #[Test]\n    public function it_checks_new_average_value_greater_than_threshold(): void\n    {\n        $condition = new AverageScoreValueCondition;\n\n        $notification = $this->makeNotification([0.5, 0.6, 0.7, 0.8], [0.7, 0.8, 0.8, 0.9]);\n\n        // Average new: (70 + 80 + 80 + 90) / 4 = 80\n        $this->assertTrue($condition->applies($notification, 'new', '>', 75, null));\n        $this->assertFalse($condition->applies($notification, 'new', '>', 80, null));\n        $this->assertTrue($condition->applies($notification, 'new', '>=', 80, null));\n    }\n\n    #[Test]\n    public function it_checks_new_average_value_less_than_threshold(): void\n    {\n        $condition = new AverageScoreValueCondition;\n\n        $notification = $this->makeNotification([0.7, 0.8, 0.8, 0.9], [0.4, 0.5, 0.5, 0.6]);\n\n        // Average new: (40 + 50 + 50 + 60) / 4 = 50\n        $this->assertTrue($condition->applies($notification, 'new', '<', 60, null));\n        $this->assertFalse($condition->applies($notification, 'new', '<', 50, null));\n        $this->assertTrue($condition->applies($notification, 'new', '<=', 50, null));\n    }\n\n    #[Test]\n    public function it_checks_old_average_value_greater_than_threshold(): void\n    {\n        $condition = new AverageScoreValueCondition;\n\n        $notification = $this->makeNotification([0.7, 0.8, 0.8, 0.9], [0.4, 0.5, 0.5, 0.6]);\n\n        // Average old: (70 + 80 + 80 + 90) / 4 = 80\n        $this->assertTrue($condition->applies($notification, 'old', '>', 75, null));\n        $this->assertFalse($condition->applies($notification, 'old', '>', 80, null));\n        $this->assertTrue($condition->applies($notification, 'old', '>=', 80, null));\n    }\n\n    #[Test]\n    public function it_checks_equality(): void\n    {\n        $condition = new AverageScoreValueCondition;\n\n        $notification = $this->makeNotification([0.5, 0.5, 0.5, 0.5], [0.6, 0.6, 0.6, 0.6]);\n\n        $this->assertTrue($condition->applies($notification, 'new', '=', 60, null));\n        $this->assertFalse($condition->applies($notification, 'new', '=', 50, null));\n        $this->assertTrue($condition->applies($notification, 'new', '<>', 50, null));\n    }\n\n    protected function makeNotification(array $oldScores, array $newScores): CategoryScoreChangedNotification\n    {\n        $monitor = LighthouseMonitor::query()->create([\n            'team_id' => 1,\n            'url' => 'https://example.com',\n            'settings' => [],\n            'interval' => '0 * * * *',\n        ]);\n\n        $result = LighthouseResult::query()->create([\n            'lighthouse_monitor_id' => $monitor->id,\n            'performance' => $newScores[0],\n            'accessibility' => $newScores[1],\n            'best_practices' => $newScores[2],\n            'seo' => $newScores[3],\n        ]);\n\n        $data = CategoryResultDifferenceData::of([\n            'performance_old' => $oldScores[0],\n            'performance_new' => $newScores[0],\n            'accessibility_old' => $oldScores[1],\n            'accessibility_new' => $newScores[1],\n            'best_practices_old' => $oldScores[2],\n            'best_practices_new' => $newScores[2],\n            'seo_old' => $oldScores[3],\n            'seo_new' => $newScores[3],\n        ]);\n\n        return new CategoryScoreChangedNotification($result, $data);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/tests/Notifications/Conditions/Category/PerformanceScoreValueConditionTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Tests\\Notifications\\Conditions\\Category;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Lighthouse\\Data\\CategoryResultDifferenceData;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Lighthouse\\Notifications\\CategoryScoreChangedNotification;\nuse Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\PerformanceScoreValueCondition;\nuse Vigilant\\Lighthouse\\Tests\\TestCase;\n\nclass PerformanceScoreValueConditionTest extends TestCase\n{\n    #[Test]\n    public function it_checks_new_value_greater_than_threshold(): void\n    {\n        $condition = new PerformanceScoreValueCondition;\n\n        $notification = $this->makeNotification(0.5, 0.8);\n\n        $this->assertTrue($condition->applies($notification, 'new', '>', 70, null));\n        $this->assertFalse($condition->applies($notification, 'new', '>', 80, null));\n        $this->assertTrue($condition->applies($notification, 'new', '>=', 80, null));\n    }\n\n    #[Test]\n    public function it_checks_new_value_less_than_threshold(): void\n    {\n        $condition = new PerformanceScoreValueCondition;\n\n        $notification = $this->makeNotification(0.8, 0.5);\n\n        $this->assertTrue($condition->applies($notification, 'new', '<', 60, null));\n        $this->assertFalse($condition->applies($notification, 'new', '<', 50, null));\n        $this->assertTrue($condition->applies($notification, 'new', '<=', 50, null));\n    }\n\n    #[Test]\n    public function it_checks_old_value_greater_than_threshold(): void\n    {\n        $condition = new PerformanceScoreValueCondition;\n\n        $notification = $this->makeNotification(0.8, 0.5);\n\n        $this->assertTrue($condition->applies($notification, 'old', '>', 70, null));\n        $this->assertFalse($condition->applies($notification, 'old', '>', 80, null));\n        $this->assertTrue($condition->applies($notification, 'old', '>=', 80, null));\n    }\n\n    #[Test]\n    public function it_checks_old_value_less_than_threshold(): void\n    {\n        $condition = new PerformanceScoreValueCondition;\n\n        $notification = $this->makeNotification(0.5, 0.8);\n\n        $this->assertTrue($condition->applies($notification, 'old', '<', 60, null));\n        $this->assertFalse($condition->applies($notification, 'old', '<', 50, null));\n        $this->assertTrue($condition->applies($notification, 'old', '<=', 50, null));\n    }\n\n    #[Test]\n    public function it_checks_equality(): void\n    {\n        $condition = new PerformanceScoreValueCondition;\n\n        $notification = $this->makeNotification(0.5, 0.5);\n\n        $this->assertTrue($condition->applies($notification, 'new', '=', 50, null));\n        $this->assertFalse($condition->applies($notification, 'new', '=', 60, null));\n        $this->assertTrue($condition->applies($notification, 'new', '<>', 60, null));\n    }\n\n    protected function makeNotification(float $oldPerformance, float $newPerformance): CategoryScoreChangedNotification\n    {\n        $monitor = LighthouseMonitor::query()->create([\n            'team_id' => 1,\n            'url' => 'https://example.com',\n            'settings' => [],\n            'interval' => '0 * * * *',\n        ]);\n\n        $result = LighthouseResult::query()->create([\n            'lighthouse_monitor_id' => $monitor->id,\n            'performance' => $newPerformance,\n            'accessibility' => 0.7,\n            'best_practices' => 0.7,\n            'seo' => 0.7,\n        ]);\n\n        $data = CategoryResultDifferenceData::of([\n            'performance_old' => $oldPerformance,\n            'performance_new' => $newPerformance,\n            'accessibility_old' => 0.7,\n            'accessibility_new' => 0.7,\n            'best_practices_old' => 0.7,\n            'best_practices_new' => 0.7,\n            'seo_old' => 0.7,\n            'seo_new' => 0.7,\n        ]);\n\n        return new CategoryScoreChangedNotification($result, $data);\n    }\n}\n"
  },
  {
    "path": "packages/lighthouse/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\Lighthouse\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Illuminate\\Support\\Facades\\Bus;\nuse Livewire\\LivewireServiceProvider;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Lighthouse\\Jobs\\RunLighthouseJob;\nuse Vigilant\\Lighthouse\\ServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n            \\Vigilant\\Core\\ServiceProvider::class,\n            \\Vigilant\\Users\\ServiceProvider::class,\n            LivewireServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n    }\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        TeamService::fake();\n        Bus::fake([\n            RunLighthouseJob::class,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/notifications/composer.json",
    "content": "{\n    \"name\": \"vigilant/notifications\",\n    \"description\": \"Vigilant Notifications\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"guzzlehttp/guzzle\": \"^7.2\",\n        \"laravel/framework\": \"^12.0\",\n        \"livewire/livewire\": \"^3.4\",\n        \"vigilant/core\": \"@dev\",\n        \"vigilant/sites\": \"@dev\",\n        \"vigilant/users\": \"@dev\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Notifications\\\\\": \"src\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Notifications\\\\Tests\\\\\": \"tests\",\n            \"Vigilant\\\\Users\\\\Database\\\\Factories\\\\\": \"../users/database/factories\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\Notifications\\\\ServiceProvider\"\n            ],\n            \"aliases\": {\n                \"NotificationRegistry\": \"Vigilant\\\\Notifications\\\\Facades\\\\NotificationRegistry\"\n            }\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/notifications/config/notifications.php",
    "content": "<?php\n\nreturn [\n\n    'queue' => 'notifications',\n\n    /*\n     * Move the old condition classes in the DB to the new ones\n     * Old class => new class\n     */\n    'moved_conditions' => [\n        \\Vigilant\\Lighthouse\\Notifications\\Conditions\\AccessibilityPercentScoreCondition::class => \\Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AccessibilityPercentScoreCondition::class,\n        \\Vigilant\\Lighthouse\\Notifications\\Conditions\\AverageScoreCondition::class => \\Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\AverageScoreCondition::class,\n        \\Vigilant\\Lighthouse\\Notifications\\Conditions\\BestPracticesPercentScoreCondition::class => \\Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\BestPracticesPercentScoreCondition::class,\n        \\Vigilant\\Lighthouse\\Notifications\\Conditions\\SeoPercentPercentScoreCondition::class => \\Vigilant\\Lighthouse\\Notifications\\Conditions\\Category\\SeoPercentPercentScoreCondition::class,\n    ],\n];\n"
  },
  {
    "path": "packages/notifications/database/migrations/2024_02_25_110000_create_notification_triggers_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('notification_triggers', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(Team::class);\n\n            $table->string('notification')->index();\n\n            $table->json('conditions');\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('notification_triggers');\n    }\n};\n"
  },
  {
    "path": "packages/notifications/database/migrations/2024_02_25_111000_create_notification_channels_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('notification_channels', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(Team::class);\n\n            $table->string('channel');\n            $table->json('settings');\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('notification_channels');\n    }\n};\n"
  },
  {
    "path": "packages/notifications/database/migrations/2024_02_25_111000_create_notification_triggers_channels_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('notification_channel_notification_trigger', function (Blueprint $table) {\n            $table->id();\n            $table->unsignedBigInteger('channel_id');\n            $table->unsignedBigInteger('trigger_id');\n\n            $table->foreign('channel_id', 'channel_id')\n                ->references('id')->on('notification_channels')->onDelete('cascade');\n            $table->foreign('trigger_id', 'trigger_id')\n                ->references('id')->on('notification_triggers')->onDelete('cascade');\n\n            $table->unique(['channel_id', 'trigger_id'], 'unique_notification_channel_trigger');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('notification_channel_notification_trigger');\n    }\n};\n"
  },
  {
    "path": "packages/notifications/database/migrations/2024_02_25_112000_create_notification_history_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Models\\Trigger;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('notification_history', function (Blueprint $table) {\n            $table->id();\n\n            $table->foreignIdFor(Channel::class)\n                ->index()\n                ->constrained('notification_channels')\n                ->cascadeOnDelete();\n\n            $table->foreignIdFor(Trigger::class)\n                ->nullable()\n                ->index()\n                ->constrained('notification_triggers')\n                ->cascadeOnDelete();\n\n            $table->string('notification');\n            $table->string('uniqueId');\n            $table->json('data');\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('notification_history');\n    }\n};\n"
  },
  {
    "path": "packages/notifications/database/migrations/2024_04_04_193000_notification_trigger_all_channels_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('notification_triggers', function (Blueprint $table): void {\n            $table->boolean('all_channels')->default(false)->after('conditions');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropColumns('notification_triggers', ['all_channels']);\n    }\n};\n"
  },
  {
    "path": "packages/notifications/database/migrations/2024_04_12_193000_notification_enabled_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('notification_triggers', function (Blueprint $table): void {\n            $table->boolean('enabled')->default(true)->after('team_id');\n            $table->string('name')->after('notification');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropColumns('notification_triggers', ['enabled', 'name']);\n    }\n};\n"
  },
  {
    "path": "packages/notifications/database/migrations/2024_09_22_113000_notification_cooldown_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('notification_triggers', function (Blueprint $table): void {\n            $table->integer('cooldown')->nullable()->after('conditions');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropColumns('notification_triggers', ['cooldown']);\n    }\n};\n"
  },
  {
    "path": "packages/notifications/database/migrations/2026_01_02_150000_add_name_to_notification_channels_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('notification_channels', function (Blueprint $table): void {\n            $table->string('name')->nullable()->after('channel');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('notification_channels', function (Blueprint $table): void {\n            $table->dropColumn('name');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/notifications/database/migrations/2026_01_02_151500_backfill_notification_channel_names.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Vigilant\\Notifications\\Channels\\NotificationChannel;\nuse Vigilant\\Notifications\\Models\\Channel;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Model::unguarded(function (): void {\n            Channel::withoutEvents(function (): void {\n                Channel::withoutGlobalScopes()\n                    ->whereNull('name')\n                    ->chunkById(100, function ($channels): void {\n                        /** @var \\Illuminate\\Support\\Collection<int, Channel> $channels */\n                        $channels->each(function (Channel $channel): void {\n                            $channelType = $channel->channel;\n\n                            if (! is_string($channelType) || ! class_exists($channelType) || ! is_subclass_of($channelType, NotificationChannel::class)) {\n                                return;\n                            }\n\n                            $channel->forceFill(['name' => $channelType::$name])->save();\n                        });\n                    });\n            });\n        });\n    }\n};\n"
  },
  {
    "path": "packages/notifications/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n    treatPhpDocTypesAsCertain: false\n    ignoreErrors:\n        - '#Unsafe usage of new static#'\n        - identifier: missingType.iterableValue\n        - identifier: missingType.generics\n        - '#return type contains unknown class Vigilant\\\\Notifications\\\\Facades\\\\Notification#'\n"
  },
  {
    "path": "packages/notifications/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/notifications/resources/navigation.php",
    "content": "<?php\n\nuse Vigilant\\Core\\Facades\\Navigation;\n\nNavigation::add(null, 'Notifications')\n    ->code('notifications')\n    ->routeIs('notifications*')\n    ->icon('tni-exclamation-circle-o')\n    ->sort(1000);\n\nNavigation::add(route('notifications'), 'Notification Types')\n    ->parent('notifications')\n    ->icon('phosphor-list-heart-duotone')\n    ->routeIs('notifications', 'notifications.trigger.*')\n    ->sort(1);\n\nNavigation::add(route('notifications.channels'), 'Notification Channels')\n    ->parent('notifications')\n    ->icon('phosphor-chat-centered-dots-bold')\n    ->routeIs('notifications.channel*')\n    ->sort(2);\n\nNavigation::add(route('notifications.history'), 'Notification History')\n    ->parent('notifications')\n    ->icon('tni-history-o')\n    ->routeIs('notifications.history')\n    ->sort(3);\n"
  },
  {
    "path": "packages/notifications/resources/views/channels.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header title=\"Notification Channels\">\n            <x-create-button dusk=\"channel-add-button\" :href=\"route('notifications.channel.create')\" model=\"Vigilant\\Notifications\\Models\\Channel\">\n                @lang('Add Channel')\n            </x-create-button>\n        </x-page-header>\n    </x-slot>\n\n    @if ($hasChannels)\n        <livewire:channel-table />\n    @else\n        <x-notifications::empty-states.channels />\n    @endif\n\n</x-app-layout>\n"
  },
  {
    "path": "packages/notifications/resources/views/components/condition-builder/condition.blade.php",
    "content": "@php($instance = app($condition['condition']))\n@php($operators = $instance->operators())\n@php($operands = $instance->operands())\n<div class=\"flex flex-wrap items-center gap-3 mt-3 p-3 rounded-lg bg-base-800 border border-base-700\" x-data=\"{ deleteHover: false }\"\n    :class=\"deleteHover ? 'ring-2 ring-red/50 border-red/50' : ''\">\n    <div class=\"flex items-center gap-2\">\n        <span class=\"block text-sm font-semibold leading-6 text-base-100\">{{ $condition['condition']::$name }}</span>\n        @if ($condition['condition']::info())\n            <span class=\"text-base-400 cursor-help hover:text-base-300 transition-colors\" title=\"{{ $condition['condition']::info() }}\">ℹ️</span>\n        @endif\n    </div>\n    @if (count($operands ?? []) > 0)\n        <div>\n            <select wire:model.live=\"children.{{ $path }}.operand\"\n                class=\"rounded-md border-0 py-1.5 pl-3 pr-10 text-base-100 bg-base-900 ring-1 ring-inset ring-base-700 focus:ring-2 focus:ring-inset focus:ring-red transition-all duration-200\">\n                @foreach ($operands as $value => $operand)\n                    <option value=\"{{ $value }}\">{{ $operand }}</option>\n                @endforeach\n            </select>\n        </div>\n    @endif\n\n    @if (count($operators ?? []) > 0)\n        <div>\n            <select wire:model.live=\"children.{{ $path }}.operator\"\n                class=\"rounded-md border-0 py-1.5 pl-3 pr-10 text-base-100 bg-base-900 ring-1 ring-inset ring-base-700 focus:ring-2 focus:ring-inset focus:ring-red transition-all duration-200\">\n                @foreach ($operators as $value => $operator)\n                    <option value=\"{{ $value }}\">{{ $operator }}</option>\n                @endforeach\n            </select>\n        </div>\n    @endif\n\n    <x-dynamic-component :component=\"$instance->type->view()\" :condition=\"$instance\" :path=\"$path\" />\n\n    <div class=\"flex-1\"></div>\n\n    <button type=\"button\" \n        wire:click=\"deletePath('{{ $path }}')\" \n        x-on:mouseover=\"deleteHover = true\"\n        x-on:mouseleave=\"deleteHover = false\"\n        class=\"cursor-pointer p-1 rounded-full hover:bg-red/20 transition-colors duration-200\">\n        @svg('tni-x-circle-o', 'w-6 h-6 text-red hover:text-red-light transition-colors')\n    </button>\n\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/components/condition-builder/group.blade.php",
    "content": "<div class=\"w-full border border-base-700 rounded-lg px-4 pt-3 pb-4 bg-base-850\" x-data=\"{ deleteHover: false, addGroupHover: false, addConditionHover: false }\"\n    :class=\"deleteHover ? 'ring-2 ring-red/50 border-red/50' : ''\">\n    <div class=\"flex flex-wrap gap-3 items-center\">\n        <div class=\"flex gap-2 items-center\">\n            <label for=\"group-operator{{ $path }}\" class=\"text-sm text-base-200 font-medium\">\n                @lang('Match')\n            </label>\n            <div>\n                <select\n                    @if (blank($path)) wire:model.live=\"parent.operator\"\n                    @else\n                        wire:model.live=\"conditions.{{ $path }}.operator\" @endif\n                    id=\"group-operator{{ $path }}\"\n                    class=\"block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-base-100 bg-base-800 ring-1 ring-inset ring-base-700 focus:ring-2 focus:ring-inset focus:ring-red transition-all duration-200\">\n                    <option value=\"any\">@lang('Any')</option>\n                    <option value=\"all\">@lang('All')</option>\n                </select>\n            </div>\n            <span>\n                <label for=\"group-operator{{ $path }}\" class=\"text-sm text-base-200\">@lang('conditions in this group')</label>\n            </span>\n        </div>\n\n        <div class=\"flex-1 flex flex-wrap gap-2 justify-end\">\n            <div class=\"min-w-[200px]\">\n                <select wire:model.live=\"selectedCondition.{{ md5($path) }}\"\n                    class=\"block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-base-100 bg-base-800 ring-1 ring-inset ring-base-700 focus:ring-2 focus:ring-inset focus:ring-red transition-all duration-200\">\n                    @foreach ($conditions as $condition)\n                        <option value=\"{{ $condition }}\">{{ $condition::$name }}</option>\n                    @endforeach\n                </select>\n            </div>\n\n            <button type=\"button\"\n                wire:click=\"addCondition('{{ $path }}')\" \n                x-on:mouseover=\"addConditionHover = true\"\n                x-on:mouseleave=\"addConditionHover = false\"\n                class=\"inline-flex items-center px-4 py-2 bg-green hover:bg-green-light text-base-100 font-medium rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green focus:ring-offset-base-900\">\n                @lang('Add Condition')\n            </button>\n\n            <button type=\"button\"\n                wire:click=\"addGroup('{{ $path }}')\" \n                x-on:mouseover=\"addGroupHover = true\"\n                x-on:mouseleave=\"addGroupHover = false\"\n                class=\"inline-flex items-center px-4 py-2.5 bg-blue text-base-100 font-semibold text-sm rounded-lg border transition-all duration-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red\">\n                @lang('Add Group')\n            </button>\n\n            @if (!blank($path))\n                <button type=\"button\"\n                    wire:click=\"deletePath('{{ $path }}')\" \n                    x-on:mouseover=\"deleteHover = true\"\n                    x-on:mouseleave=\"deleteHover = false\"\n                    class=\"inline-flex items-center px-4 py-2.5 bg-red text-base-100 font-semibold text-sm rounded-lg border transition-all duration-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red\">\n                    @lang('Delete Group')\n                </button>\n            @endif\n        </div>\n    </div>\n\n    <div class=\"px-1\">\n        @foreach ($children as $index => $child)\n            @if ($child['type'] === 'group')\n                <div class=\"px-4 pt-4\">\n                    <x-notifications::condition-builder.group :children=\"$child['children']\" :conditions=\"$conditions\" :path=\"blank($path) ? $index : $path . '.children.' . $index\" />\n                </div>\n            @endif\n\n            @if ($child['type'] === 'condition')\n                <div class=\"px-2\">\n                    <x-notifications::condition-builder.condition :condition=\"$child\" :path=\"blank($path) ? $index : $path . '.children.' . $index\" />\n                </div>\n            @endif\n        @endforeach\n\n        <div class=\"w-full bg-blue/20 border border-blue text-blue-light rounded-md py-2 px-3 mt-3 text-sm font-medium pointer-events-none\" x-cloak x-show=\"addGroupHover\">\n            @lang('New Group')\n        </div>\n        <div class=\"w-full bg-green/20 border border-green text-green-light rounded-md py-2 px-3 mt-3 text-sm font-medium pointer-events-none\" x-cloak x-show=\"addConditionHover\">\n            @lang('New Condition')\n        </div>\n\n    </div>\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/components/condition-builder/type/number.blade.php",
    "content": "<div>\n    <input type=\"number\" wire:model.live=\"children.{{ $path }}.value\"\n        class=\"rounded-md bg-base-900 border-0 ring-1 ring-inset ring-base-700 focus:ring-2 focus:ring-inset focus:ring-red py-1.5 px-3 text-base-100 focus:ring-0 sm:text-sm sm:leading-6 transition-all duration-200\">\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/components/condition-builder/type/select.blade.php",
    "content": "<div>\n    <select\n        wire:model.live=\"children.{{ $path }}.value\"\n        class=\"rounded-md border-0 py-1.5 pl-3 pr-10 text-base-100 bg-base-900 ring-1 ring-inset ring-base-700 focus:ring-2 focus:ring-inset focus:ring-red transition-all duration-200\">\n        @foreach($condition->options() as $value => $name)\n            <option value=\"{{ $value }}\">{{ $name }}</option>\n        @endforeach\n    </select>\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/components/condition-builder/type/static.blade.php",
    "content": "<div>\n    {{-- Static conditions don't require any input --}}\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/components/condition-builder/type/text.blade.php",
    "content": "<div>\n    <input type=\"text\" wire:model.live=\"children.{{ $path }}.value\"\n        class=\"rounded-md bg-base-900 border-0 ring-1 ring-inset ring-base-700 focus:ring-2 focus:ring-inset focus:ring-red py-1.5 px-3 text-base-100 focus:ring-0 sm:text-sm sm:leading-6 transition-all duration-200\">\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/components/empty-states/channels.blade.php",
    "content": "<x-frontend::empty-state\n    :title=\"__('No Notification Channels')\"\n    :description=\"__('Connect email, chat, or webhook channels to deliver alerts to your team.')\"\n    icon=\"phosphor-warning-circle\"\n    iconClass=\"h-12 w-12 text-sky\"\n    iconWrapperClass=\"rounded-full bg-sky/10 p-4 mb-6\"\n    :buttonHref=\"route('notifications.channel.create')\"\n    :buttonText=\"__('Add Channel')\"\n    buttonClass=\"bg-sky hover:bg-sky/90 text-base-50 px-5 py-2.5 rounded-lg transition-all duration-300\"\n/>\n"
  },
  {
    "path": "packages/notifications/resources/views/history.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header title=\"Notification History\"/>\n    </x-slot>\n\n    <livewire:notification-history-table/>\n\n</x-app-layout>\n"
  },
  {
    "path": "packages/notifications/resources/views/livewire/channels/configuration/discord.blade.php",
    "content": "<div>\n    <x-form.text\n        field=\"settings.webhook_url\"\n        name=\"Webhook URL\"\n        description=\"Discord Webhook URL\"\n    />\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/livewire/channels/configuration/google-chat.blade.php",
    "content": "<div>\n    <x-form.text field=\"settings.webhook_url\" name=\"Webhook URL\" description=\"Google Chat Webhook URL\" />\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/livewire/channels/configuration/mail.blade.php",
    "content": "<div>\n    <x-form.text\n        field=\"settings.to\"\n        name=\"To\"\n        description=\"E-mail address of the recipient\"\n    />\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/livewire/channels/configuration/microsoft-teams.blade.php",
    "content": "<div>\n    <x-form.text field=\"settings.webhook_url\" name=\"Webhook URL\" description=\"Teams Webhook URL\" />\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/livewire/channels/configuration/ntfy.blade.php",
    "content": "<div>\n    <x-form.text\n        field=\"settings.server\"\n        name=\"NTFY Server\"\n        description=\"URL of the NTFY server\"\n    />\n\n    <x-form.text\n        field=\"settings.topic\"\n        name=\"NTFY Topic\"\n        description=\"Notification Topic\"\n    />\n\n    <x-form.select\n        field=\"settings.auth_method\"\n        name=\"Authentication Method\"\n        description=\"Choose the authentication method\"\n    >\n        <option value=\"\" selected>None</option>\n        <option value=\"username\">Username and Password</option>\n        <option value=\"token\">Token</option>\n    </x-form.select>\n\n    @if (($settings['auth_method'] ?? '') === 'username')\n        <x-form.text\n                field=\"settings.username\"\n                name=\"Username\"\n        />\n\n        <x-form.password\n                field=\"settings.password\"\n                name=\"Password\"\n        />\n    @endif\n\n    @if (($settings['auth_method'] ?? '') === 'token')\n        <x-form.text\n                field=\"settings.token\"\n                name=\"Access Token\"\n        />\n    @endif\n\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/livewire/channels/configuration/slack.blade.php",
    "content": "<div>\n    <x-form.text\n        field=\"settings.webhook_url\"\n        name=\"Webhook URL\"\n        description=\"Slack Webhook URL\"\n    />\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/livewire/channels/configuration/telegram.blade.php",
    "content": "<div>\n    <x-form.text\n        field=\"settings.bot_token\"\n        name=\"Bot token\"\n        description=\"Telegram Bot token from @BotFather\"\n    />\n\n    <x-form.text\n        field=\"settings.chat_id\"\n        name=\"Chat ID\"\n        description=\"Your Telegram chat ID\"\n    />\n\n    <div class=\"mt-4 text-sm text-gray-600 dark:text-gray-400\">\n        <p class=\"font-medium mb-2\">How to get your chat ID:</p>\n        <ol class=\"list-decimal list-inside space-y-1\">\n            <li>Send a message to your bot on Telegram</li>\n            <li>Visit <code class=\"px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded\">https://api.telegram.org/bot{your_bot_token}/getUpdates</code></li>\n            <li>Look for the <code class=\"px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded\">chat</code> → <code class=\"px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded\">id</code> field in the response</li>\n        </ol>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/livewire/channels/configuration/webhook.blade.php",
    "content": "<div>\n    <x-form.text\n        field=\"settings.url\"\n        name=\"URL\"\n        description=\"URL of the webhook\"\n    />\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/livewire/channels/form.blade.php",
    "content": "<div>\n    @if (!$inline)\n        <x-slot name=\"header\">\n            <x-page-header :title=\"$updating ? 'Edit Channel - ' . $channelModel->title() : 'Add Channel'\" :back=\"route('notifications.channels')\">\n                @if($updating)\n                    <x-frontend::page-header.actions>\n                        <x-form.button class=\"bg-red\" @click=\"$dispatch('open-delete-modal')\">\n                            @lang('Delete')\n                        </x-form.button>\n                    </x-frontend::page-header.actions>\n                    \n                    <x-frontend::page-header.mobile-actions>\n                        <x-form.dropdown-button class=\"!text-red hover:!text-red-light\" @click=\"$dispatch('open-delete-modal')\">\n                            @lang('Delete')\n                        </x-form.dropdown-button>\n                    </x-frontend::page-header.mobile-actions>\n                @endif\n            </x-page-header>\n        </x-slot>\n    @endif\n\n    <form wire:submit=\"save\">\n        <div class=\"max-w-7xl mx-auto\">\n            <x-card>\n                <div class=\"flex flex-col gap-4\">\n                    @if ($testSent)\n                        <x-alerts.info :title=\"__('Test notification sent')\" />\n                    @endif\n\n                    <x-form.select field=\"form.channel\" name=\"Channel\" description=\"Choose the notification channel\">\n                        <option value=\"\" disabled selected>--- Select ---</option>\n\n                        @foreach (\\Vigilant\\Notifications\\Facades\\NotificationRegistry::channels() as $channel)\n                            <option value=\"{{ $channel }}\">{{ $channel::$name }}</option>\n                        @endforeach\n                    </x-form.select>\n\n                    <x-form.text field=\"form.name\" name=\"Internal Name\"\n                        description=\"Give this channel a recognizable label for your team.\">\n                    </x-form.text>\n\n                    <h3 class=\"text-lg font-bold leading-7 sm:truncate sm:text-2xl sm:tracking-tight text-neutral-100\">\n                        {{ __('Configuration') }}</h3>\n\n                    @if ($settingsComponent !== null)\n                        @livewire($settingsComponent, ['channel' => $this->form->channel, 'settings' => $channelModel?->settings ?? []], key($this->form->channel))\n                    @else\n                        <span class=\"text-xs text-neutral-400\">{{ __('Select a channel to configure') }}</span>\n                    @endif\n\n                    <x-form.submit-button dusk=\"submit-button\" :submitText=\"$updating ? 'Save' : 'Create'\">\n                        <x-form.button wire:click=\"test\">Test</x-form.button>\n                    </x-form.submit-button>\n                </div>\n            </x-card>\n        </div>\n    </form>\n\n    <!-- Delete Confirmation Modal -->\n    @if($updating && !$inline)\n        <div x-data=\"{ showDeleteModal: false }\" @open-delete-modal.window=\"showDeleteModal = true\">\n            <x-frontend::modal show=\"showDeleteModal\">\n                <x-frontend::modal.header icon=\"phosphor-trash\" iconColor=\"red\" show=\"showDeleteModal\">\n                    @lang('Delete Notification Channel')\n                </x-frontend::modal.header>\n\n                <x-frontend::modal.body>\n                    <div class=\"space-y-4\">\n                        <p class=\"text-base-100\">\n                            @lang('Are you sure you want to delete this notification channel?')\n                        </p>\n                        <div class=\"bg-base-850 border border-base-700 rounded-lg p-4\">\n                            <div class=\"flex items-start gap-3\">\n                                <div class=\"flex-shrink-0\">\n                                    @svg('phosphor-warning-circle', 'w-5 h-5 text-orange mt-0.5')\n                                </div>\n                                <div class=\"flex-1\">\n                                    <p class=\"text-sm text-base-300\">\n                                        <span class=\"font-semibold text-base-100\">{{ $channelModel->title() }}</span>\n                                    </p>\n                                    <p class=\"text-sm text-base-400 mt-1\">\n                                        @lang('This action cannot be undone. All channel settings will be permanently deleted and notifications will no longer be sent to this channel.')\n                                    </p>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </x-frontend::modal.body>\n\n                <x-frontend::modal.footer>\n                    <x-form.button type=\"button\" @click=\"showDeleteModal = false\">\n                        @lang('Cancel')\n                    </x-form.button>\n                    <x-form.button class=\"bg-red\" type=\"button\" wire:click=\"delete\">\n                        @lang('Delete Channel')\n                    </x-form.button>\n                </x-frontend::modal.footer>\n            </x-frontend::modal>\n        </div>\n    @endif\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/livewire/notifications/condition-builder.blade.php",
    "content": "<div>\n    <x-notifications::condition-builder.group :children=\"$children\" :conditions=\"$conditions\" path=\"\"/>\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/livewire/notifications/form.blade.php",
    "content": "<div>\n    <x-slot name=\"header\">\n        <x-page-header :title=\"$updating ? 'Edit Notification - ' . $trigger->notification::$name : 'Add Notification'\" :back=\"route('notifications')\">\n            @if($updating)\n                <x-frontend::page-header.actions>\n                    <x-form.button class=\"bg-red\" @click=\"$dispatch('open-delete-modal')\">\n                        @lang('Delete')\n                    </x-form.button>\n                </x-frontend::page-header.actions>\n                \n                <x-frontend::page-header.mobile-actions>\n                    <x-form.dropdown-button class=\"!text-red hover:!text-red-light\" @click=\"$dispatch('open-delete-modal')\">\n                        @lang('Delete')\n                    </x-form.dropdown-button>\n                </x-frontend::page-header.mobile-actions>\n            @endif\n        </x-page-header>\n    </x-slot>\n\n    <div class=\"max-w-7xl mx-auto space-y-6\">\n        <!-- Main Settings Card -->\n        <form wire:submit=\"save\">\n            <x-card>\n                <div class=\"flex flex-col gap-4\">\n                    <x-form.checkbox field=\"form.enabled\" name=\"Enabled\" description=\"Enable or disable this lighthouse monitor\" />\n\n                    <x-form.text field=\"form.name\" name=\"Name\" description=\"Name this notification\">\n                    </x-form.text>\n\n                    <x-form.select field=\"form.notification\" name=\"Trigger\" :disabled=\"$updating\"\n                        description=\"Choose the event that triggers this notification\">\n                        <option value=\"\" disabled selected>--- Select ---</option>\n\n                        @foreach (\\Vigilant\\Notifications\\Facades\\NotificationRegistry::notifications() as $notification)\n                            <option value=\"{{ $notification }}\">{{ $notification::$name }}</option>\n                        @endforeach\n                    </x-form.select>\n\n                    @if ($form->notification && $form->notification::info())\n                        <div class=\"grid grid-cols-2\">\n                            <div></div>\n                            <p class=\"mt-1 text-sm text-base-300 flex items-start gap-1\">\n                                <span class=\"shrink-0\">ℹ️</span>\n                                <span>{{ $form->notification::info() }}</span>\n                            </p>\n                        </div>\n                    @endif\n\n                    <x-form.number field=\"form.cooldown\" name=\"Cooldown\"\n                        description=\"Amount of minutes between sending notifications\">\n                    </x-form.number>\n\n                    <x-form.checkbox field=\"form.all_channels\" name=\"Sent on all channels\"\n                        description=\"Send this notification to all channels\">\n                    </x-form.checkbox>\n\n                    <x-form.select field=\"channels\" name=\"Channels\"\n                        description=\"Choose the channels that this notification should be sent to\" multiple :disabled=\"$form->all_channels\">\n                        @foreach (\\Vigilant\\Notifications\\Models\\Channel::query()->get() as $channel)\n                            <option value=\"{{ $channel->id }}\">{{ $channel->title() }}</option>\n                        @endforeach\n                    </x-form.select>\n\n                    <x-form.submit-button dusk=\"submit-button\" :submitText=\"$updating ? 'Save Settings' : 'Create Notification'\" />\n                </div>\n            </x-card>\n        </form>\n\n        <!-- Condition Builder Card -->\n        @if ($updating)\n            <form wire:submit=\"save\">\n                <x-card>\n                    <div class=\"flex flex-col gap-4\">\n                        <div>\n                            <h3 class=\"text-lg font-bold text-base-100\">@lang('Conditions')</h3>\n                            <p class=\"text-sm text-base-400 mt-1\">@lang('Only notify when these conditions match')</p>\n                        </div>\n                        \n                        <livewire:notification-condition-builder :notification=\"$trigger->notification\" :initial=\"$form->conditions\" />\n\n                        <x-form.submit-button dusk=\"submit-conditions-button\" submitText=\"Save Conditions\" />\n                    </div>\n                </x-card>\n            </form>\n        @endif\n    </div>\n\n    <!-- Delete Confirmation Modal -->\n    @if($updating)\n        <div x-data=\"{ showDeleteModal: false }\" @open-delete-modal.window=\"showDeleteModal = true\">\n            <x-frontend::modal show=\"showDeleteModal\">\n                <x-frontend::modal.header icon=\"phosphor-trash\" iconColor=\"red\" show=\"showDeleteModal\">\n                    @lang('Delete Notification Trigger')\n                </x-frontend::modal.header>\n\n                <x-frontend::modal.body>\n                    <div class=\"space-y-4\">\n                        <p class=\"text-base-100\">\n                            @lang('Are you sure you want to delete this notification trigger?')\n                        </p>\n                        <div class=\"bg-base-850 border border-base-700 rounded-lg p-4\">\n                            <div class=\"flex items-start gap-3\">\n                                <div class=\"flex-shrink-0\">\n                                    @svg('phosphor-warning-circle', 'w-5 h-5 text-orange mt-0.5')\n                                </div>\n                                <div class=\"flex-1\">\n                                    <p class=\"text-sm text-base-300\">\n                                        <span class=\"font-semibold text-base-100\">{{ $form->name }}</span>\n                                    </p>\n                                    <p class=\"text-sm text-base-400 mt-1\">\n                                        @lang('This action cannot be undone. All trigger conditions and settings will be permanently deleted.')\n                                    </p>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </x-frontend::modal.body>\n\n                <x-frontend::modal.footer>\n                    <x-form.button type=\"button\" @click=\"showDeleteModal = false\">\n                        @lang('Cancel')\n                    </x-form.button>\n                    <x-form.button class=\"bg-red\" type=\"button\" wire:click=\"delete\">\n                        @lang('Delete Trigger')\n                    </x-form.button>\n                </x-frontend::modal.footer>\n            </x-frontend::modal>\n        </div>\n    @endif\n</div>\n"
  },
  {
    "path": "packages/notifications/resources/views/mails/notification.blade.php",
    "content": "<p>{{ $description }}</p>\n\n@if($viewUrl !== null)\n    <a href=\"{{ $viewUrl }}\">@lang('View in Vigilant')</a>\n@endif\n\n@if($url !== null && $urlTitle !== null)\n    <a href=\"{{ $url }}\">@lang($urlTitle)</a>\n@endif\n\n"
  },
  {
    "path": "packages/notifications/resources/views/notifications.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header title=\"Notifications\">\n            <x-create-button dusk=\"trigger-add-button\" :href=\"route('notifications.trigger.create')\" model=\"Vigilant\\Notifications\\Models\\Trigger\">\n                @lang('Add Notification')\n            </x-create-button>\n        </x-page-header>\n    </x-slot>\n\n    <livewire:notification-table />\n</x-app-layout>\n"
  },
  {
    "path": "packages/notifications/routes/web.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\Notifications\\Http\\Controllers\\ChannelController;\nuse Vigilant\\Notifications\\Http\\Livewire\\ChannelForm;\nuse Vigilant\\Notifications\\Http\\Livewire\\NotificationForm;\n\nRoute::prefix('notifications')->group(function () {\n    Route::view('/', 'notifications::notifications')->name('notifications');\n    Route::get('create', NotificationForm::class)->name('notifications.trigger.create');\n    Route::get('edit/{trigger}', NotificationForm::class)->name('notifications.trigger.edit');\n\n    Route::prefix('channels')->group(function () {\n        Route::get('/', [ChannelController::class, 'index'])->name('notifications.channels');\n        Route::get('create', ChannelForm::class)->name('notifications.channel.create');\n        Route::get('edit/{channel}', ChannelForm::class)->name('notifications.channel.edit');\n    });\n\n    Route::view('history', 'notifications::history')->name('notifications.history');\n});\n"
  },
  {
    "path": "packages/notifications/src/Actions/CheckBurst.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Actions;\n\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Models\\Trigger;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass CheckBurst\n{\n    protected const int BURST_SECONDS = 30;\n\n    public function isBursting(Notification $notification, Trigger $trigger, Channel $channel): bool\n    {\n        $key = $this->cacheKey($notification, $trigger, $channel);\n\n        return ! cache()->add($key, 1, self::BURST_SECONDS);\n    }\n\n    protected function cacheKey(Notification $notification, Trigger $trigger, Channel $channel): string\n    {\n        return sprintf('notifications:burst:%s:%s:%s:%s', $notification::class, $notification->uniqueId(), $trigger->id, $channel->id);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Actions/CheckCooldown.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Actions;\n\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Models\\History;\nuse Vigilant\\Notifications\\Models\\Trigger;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass CheckCooldown\n{\n    public function onCooldown(Trigger $trigger, Channel $channel, Notification $notification): bool\n    {\n        $cooldown = $trigger->cooldown;\n\n        if ($cooldown === null) {\n            $cooldown = $notification::$defaultCooldown;\n        }\n\n        if ($cooldown === null || $cooldown === 0) {\n            return false;\n        }\n\n        /** @var ?History $lastNotification */\n        $lastNotification = $channel->history()\n            ->where('trigger_id', '=', $trigger->id)\n            ->where('uniqueId', '=', $notification->uniqueId())\n            ->orderByDesc('created_at')\n            ->first();\n\n        if ($lastNotification === null) {\n            return false;\n        }\n\n        $minutesSinceLastNotification = $lastNotification->created_at?->diffInMinutes() ?? 0;\n\n        return $minutesSinceLastNotification < $cooldown;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Actions/CreateNotifications.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Actions;\n\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Notifications\\Models\\Trigger;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Users\\Models\\Team;\n\nclass CreateNotifications\n{\n    public function create(Team $team): void\n    {\n        $notifications = NotificationRegistry::notifications();\n\n        /** @var class-string<Notification> $notification */\n        foreach ($notifications as $notification) {\n            if (! $notification::$autoCreate) {\n                continue;\n            }\n\n            Trigger::query()->firstOrCreate([\n                'team_id' => $team->id,\n                'notification' => $notification,\n            ], [\n                'enabled' => true,\n                'name' => $notification::$name,\n                'all_channels' => true,\n                'conditions' => $notification::$defaultConditions,\n                'cooldown' => $notification::$defaultCooldown,\n            ]);\n        }\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Channels/DiscordChannel.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Channels;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass DiscordChannel extends NotificationChannel\n{\n    public static string $name = 'Discord';\n\n    public static ?string $component = 'channel-configuration-discord';\n\n    public array $rules = [\n        'webhook_url' => ['required', 'url', 'starts_with:https://discord.com/api/webhooks'],\n    ];\n\n    public function fire(Notification $notification, Channel $channel): void\n    {\n        $settings = $channel->settings;\n\n        $description = $notification->description();\n\n        if ($viewUrl = $notification->viewUrl()) {\n            $description .= __(\"\\n\\n[View in Vigilant](:url)\", ['url' => $viewUrl]);\n        }\n\n        $fields = [];\n\n        if (($url = $notification->url()) && ($urlTitle = $notification->urlTitle())) {\n            $fields[] = [\n                'name' => $urlTitle,\n                'value' => __('[Click here](:url)', ['url' => $url]),\n                'inline' => true,\n            ];\n        }\n\n        $payload = [\n            'embeds' => [\n                [\n                    'title' => $notification->title(),\n                    'description' => $description,\n                    'color' => $notification->level()->color(),\n                    'fields' => $fields,\n                ],\n            ],\n        ];\n\n        Http::post($settings['webhook_url'], $payload);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Channels/GoogleChatChannel.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Channels;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass GoogleChatChannel extends NotificationChannel\n{\n    public static string $name = 'Google Chat';\n\n    public static ?string $component = 'channel-configuration-google-chat';\n\n    public array $rules = [\n        'webhook_url' => ['required', 'url', 'starts_with:https://chat.googleapis.com/v1/spaces/'],\n    ];\n\n    public function fire(Notification $notification, Channel $channel): void\n    {\n        $settings = $channel->settings;\n\n        $description = $notification->description();\n\n        if ($viewUrl = $notification->viewUrl()) {\n            $description .= \"\\n\\n[View in Vigilant]($viewUrl)\";\n        }\n\n        $lines = [];\n\n        $lines[] = '*'.$notification->title().'*';\n        $lines[] = $description;\n\n        if (($url = $notification->url()) && ($urlTitle = $notification->urlTitle())) {\n            $lines[] = \"\\n[\".$urlTitle.\"]($url)\";\n        }\n\n        $payload = [\n            'text' => implode(\"\\n\", $lines),\n        ];\n\n        Http::post($settings['webhook_url'], $payload);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Channels/MailChannel.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Channels;\n\nuse Illuminate\\Support\\Facades\\Mail;\nuse Vigilant\\Notifications\\Mail\\NotificationMail;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass MailChannel extends NotificationChannel\n{\n    public static string $name = 'Mail';\n\n    public static ?string $component = 'channel-configuration-mail';\n\n    public array $rules = [\n        'to' => ['required', 'email'],\n    ];\n\n    public function fire(Notification $notification, Channel $channel): void\n    {\n        $settings = $channel->settings;\n\n        /** @var string $to */\n        $to = $settings['to'];\n\n        Mail::to($to)->send(new NotificationMail($notification));\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Channels/MicrosoftTeamsChannel.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Channels;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass MicrosoftTeamsChannel extends NotificationChannel\n{\n    public static string $name = 'Microsoft Teams';\n\n    public static ?string $component = 'channel-configuration-microsoft-teams';\n\n    public array $rules = [\n        'webhook_url' => ['required', 'url', 'starts_with:https://outlook.office.com/webhook/'],\n    ];\n\n    public function fire(Notification $notification, Channel $channel): void\n    {\n        $settings = $channel->settings;\n\n        $description = $notification->description();\n\n        if ($viewUrl = $notification->viewUrl()) {\n            $description .= \"\\n\\n[View in Vigilant]($viewUrl)\";\n        }\n\n        $facts = [];\n\n        if (($url = $notification->url()) && ($urlTitle = $notification->urlTitle())) {\n            $facts[] = [\n                'name' => $urlTitle,\n                'value' => \"[Click here]($url)\",\n            ];\n        }\n\n        $payload = [\n            '@type' => 'MessageCard',\n            '@context' => 'http://schema.org/extensions',\n            'summary' => $notification->title(),\n            'themeColor' => $notification->level()->color(),\n            'title' => $notification->title(),\n            'text' => $description,\n            'sections' => count($facts) ? [\n                [\n                    'facts' => $facts,\n                ],\n            ] : [],\n        ];\n\n        Http::post($settings['webhook_url'], $payload);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Channels/NotificationChannel.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Channels;\n\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nabstract class NotificationChannel\n{\n    public static string $name = '';\n\n    /** @var array - Validation rules for settings */\n    public array $rules = [];\n\n    /** @var ?string Livewire component for configuring the channel */\n    public static ?string $component = null;\n\n    public function rules(): array\n    {\n        return $this->rules;\n    }\n\n    abstract public function fire(Notification $notification, Channel $channel): void;\n}\n"
  },
  {
    "path": "packages/notifications/src/Channels/NtfyChannel.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Channels;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass NtfyChannel extends NotificationChannel\n{\n    public static string $name = 'Ntfy';\n\n    public static ?string $component = 'channel-configuration-ntfy';\n\n    public array $rules = [\n        'server' => ['required', 'url'],\n        'topic' => ['required'],\n        'auth_method' => ['nullable', 'in:username,token'],\n        'username' => ['required_if:auth_method,username'],\n        'password' => ['required_if:auth_method,username'],\n        'token' => ['required_if:auth_method,token'],\n    ];\n\n    public function fire(Notification $notification, Channel $channel): void\n    {\n        $settings = $channel->settings;\n\n        $tag = match ($notification->level()) {\n            Level::Info => 'grey_exclamation',\n            Level::Warning => 'warning',\n            Level::Critical => 'triangular_flag_on_post',\n            Level::Success => 'white_check_mark',\n        };\n\n        $request = Http::baseUrl($settings['server'])\n            ->withHeaders([\n                'Title' => $notification->title().' - '.config('app.name'),\n                'Tags' => $tag,\n            ]);\n\n        $viewUrl = $notification->viewUrl();\n        $url = $notification->url();\n        $urlTitle = $notification->urlTitle();\n\n        if ($viewUrl !== null) {\n            $request->withHeader('click', $viewUrl);\n        }\n\n        if ($url !== null && $urlTitle !== null) {\n            $request->withHeader('Actions', \"view, $urlTitle, $url, clear=true\");\n        }\n\n        if ($settings['auth_method'] === 'username') {\n            $request->withBasicAuth(\n                $settings['username'],\n                $settings['password'],\n            );\n        }\n\n        if ($settings['auth_method'] === 'token') {\n            $request->withToken($settings['token']);\n        }\n\n        $request\n            ->withBody($notification->description())\n            ->post($settings['topic']);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Channels/SlackChannel.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Channels;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass SlackChannel extends NotificationChannel\n{\n    public static string $name = 'Slack';\n\n    public static ?string $component = 'channel-configuration-slack';\n\n    public array $rules = [\n        'webhook_url' => ['required', 'url'],\n    ];\n\n    public function fire(Notification $notification, Channel $channel): void\n    {\n        $settings = $channel->settings;\n\n        $blocks = [];\n\n        $blocks[] = [\n            'type' => 'section',\n            'text' => [\n                'type' => 'mrkdwn',\n                'text' => \"*{$notification->title()}*\\n{$notification->description()}\",\n            ],\n        ];\n\n        if ($viewUrl = $notification->viewUrl()) {\n            $blocks[] = [\n                'type' => 'actions',\n                'elements' => [\n                    [\n                        'type' => 'button',\n                        'text' => [\n                            'type' => 'plain_text',\n                            'text' => __('View in Vigilant'),\n                            'emoji' => true,\n                        ],\n                        'url' => $viewUrl,\n                        'action_id' => 'view_more_button',\n                    ],\n                ],\n            ];\n        }\n\n        if (($url = $notification->url()) && ($urlTitle = $notification->urlTitle())) {\n            $blocks[] = [\n                'type' => 'actions',\n                'elements' => [\n                    [\n                        'type' => 'button',\n                        'text' => [\n                            'type' => 'plain_text',\n                            'text' => __($urlTitle),\n                            'emoji' => true,\n                        ],\n                        'url' => $url,\n                        'action_id' => 'action_button',\n                    ],\n                ],\n            ];\n        }\n\n        $payload = [\n            'blocks' => $blocks,\n            'attachments' => [\n                [\n                    'color' => $notification->level()->color(),\n                ],\n            ],\n        ];\n\n        Http::post($settings['webhook_url'], $payload);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Channels/TelegramChannel.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Channels;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass TelegramChannel extends NotificationChannel\n{\n    public static string $name = 'Telegram';\n\n    public static ?string $component = 'channel-configuration-telegram';\n\n    public array $rules = [\n        'bot_token' => ['required', 'string'],\n        'chat_id' => ['required', 'string'],\n    ];\n\n    public function fire(Notification $notification, Channel $channel): void\n    {\n        $settings = $channel->settings;\n\n        $title = $this->escapeMarkdownV2($notification->title());\n        $description = $this->escapeMarkdownV2($notification->description());\n        $text = \"*{$title}*\\n\\n{$description}\";\n\n        $inlineKeyboard = [];\n\n        if ($viewUrl = $notification->viewUrl()) {\n            $inlineKeyboard[] = [\n                [\n                    'text' => __('View in Vigilant'),\n                    'url' => $viewUrl,\n                ],\n            ];\n        }\n\n        if (($url = $notification->url()) && ($urlTitle = $notification->urlTitle())) {\n            $inlineKeyboard[] = [\n                [\n                    'text' => __($urlTitle),\n                    'url' => $url,\n                ],\n            ];\n        }\n\n        $payload = [\n            'chat_id' => $settings['chat_id'],\n            'text' => $text,\n            'parse_mode' => 'MarkdownV2',\n        ];\n\n        if (! empty($inlineKeyboard)) {\n            $payload['reply_markup'] = [\n                'inline_keyboard' => $inlineKeyboard,\n            ];\n        }\n\n        $url = \"https://api.telegram.org/bot{$settings['bot_token']}/sendMessage\";\n\n        Http::post($url, $payload);\n    }\n\n    protected function escapeMarkdownV2(string $text): string\n    {\n        return preg_replace('/([_*\\[\\]()~`>#+\\-=|{}.!\\\\\\\\])/', '\\\\\\\\$1', $text) ?? '';\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Channels/WebhookChannel.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Channels;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass WebhookChannel extends NotificationChannel\n{\n    public static string $name = 'Webhook';\n\n    public static ?string $component = 'channel-configuration-webhook';\n\n    public array $rules = [\n        'url' => ['required', 'url'],\n    ];\n\n    public function fire(Notification $notification, Channel $channel): void\n    {\n        Http::post($channel->settings['url'], [\n            'level' => $notification->level(),\n            'title' => $notification->title(),\n            'description' => $notification->description(),\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Commands/CreateNotificationsCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Foundation\\Bus\\PendingDispatch;\nuse Vigilant\\Notifications\\Jobs\\CreateNotificationsJob;\nuse Vigilant\\Users\\Models\\Team;\n\nclass CreateNotificationsCommand extends Command\n{\n    protected $signature = 'notifications:create {teamId?}';\n\n    protected $description = 'Create notifications in DB for teams';\n\n    public function handle(): int\n    {\n        /** @var ?int $teamId */\n        $teamId = $this->argument('teamId');\n\n        Team::query()\n            ->when($teamId !== null, fn (Builder $builder): Builder => $builder->where('team_id', '=', $teamId))\n            ->get()\n            ->each(fn (Team $team): PendingDispatch => CreateNotificationsJob::dispatch($team));\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Commands/RenameConditionClassesCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Support\\Str;\nuse Vigilant\\Notifications\\Models\\Trigger;\n\nclass RenameConditionClassesCommand extends Command\n{\n    protected $signature = 'notifications:rename-classes';\n\n    protected $description = 'Rename moved classes';\n\n    public function handle(): int\n    {\n        /** @var array<string, string> $renamed */\n        $renamed = config('notifications.moved_conditions');\n\n        foreach ($renamed as $old => $new) {\n            $this->info(\"Checking $old => $new\");\n\n            $oldClass = Str::replace('\\\\', '\\\\\\\\\\\\\\\\', $old);\n\n            $triggers = Trigger::query()\n                ->withoutGlobalScopes()\n                ->whereRaw(\"JSON_UNQUOTE(JSON_EXTRACT(conditions, '$')) LIKE ?\", [\"%$oldClass%\"])\n                ->get();\n\n            $this->info(\"Found {$triggers->count()} triggers to update\");\n\n            foreach ($triggers as $trigger) {\n                $trigger->update([\n                    'conditions' => $this->processGroup($trigger->conditions, $old, $new),\n                ]);\n            }\n\n            $this->newLine();\n        }\n\n        return static::SUCCESS;\n    }\n\n    protected function processGroup(array $group, string $old, string $new): array\n    {\n        foreach ($group['children'] ?? [] as $index => $child) {\n            if ($child['type'] === 'condition') {\n                if ($child['condition'] === $old) {\n                    data_set($group, \"children.$index.condition\", $new);\n                }\n            }\n\n            if ($child['type'] === 'group') {\n                data_set($group, \"children.$index\", $this->processGroup($child, $old, $new));\n            }\n        }\n\n        return $group;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Commands/TestNotificationCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Jobs\\SendNotificationJob;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Notifications\\TestNotification;\n\nclass TestNotificationCommand extends Command\n{\n    protected $signature = 'notifications:test-channel {channelId}';\n\n    protected $description = 'Test a notification channel';\n\n    public function handle(): int\n    {\n        /** @var ?int $channelId */\n        $channelId = $this->argument('channelId');\n\n        /** @var Channel $channel */\n        $channel = Channel::query()->withoutGlobalScopes()->findOrFail($channelId);\n\n        foreach (Level::cases() as $level) {\n            SendNotificationJob::dispatchSync(\n                new TestNotification($level),\n                $channel->team_id,\n                $channel->id\n            );\n        }\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Concerns/NotificationFake.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Concerns;\n\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\ntrait NotificationFake\n{\n    protected static bool $faked = false;\n\n    /** @var array<int, Notification> */\n    protected static array $fakeDispatches = [];\n\n    public static function fake(): void\n    {\n        static::$faked = true;\n    }\n\n    public static function wasDispatched(?\\Closure $callback = null): bool\n    {\n        if ($callback !== null) {\n            foreach (static::$fakeDispatches as $dispatch) {\n                if (! $callback($dispatch)) {\n                    return false;\n                }\n            }\n\n            return true;\n        }\n\n        return count(static::$fakeDispatches) > 0;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Conditions/Condition.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Conditions;\n\nuse Vigilant\\Notifications\\Enums\\ConditionType;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nabstract class Condition\n{\n    public static string $name = '';\n\n    public ConditionType $type = ConditionType::Text;\n\n    /** @param array<string, mixed> $meta */\n    abstract public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool;\n\n    /** @return array<string, string> */\n    public function operators(): array\n    {\n        return [];\n    }\n\n    /** @return array<string, string> */\n    public function operands(): array\n    {\n        return [];\n    }\n\n    /** @return array<string, mixed> */\n    public function metadata(): array\n    {\n        return [];\n    }\n\n    public static function info(): ?string\n    {\n        return null;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Conditions/ConditionEngine.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Conditions;\n\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass ConditionEngine\n{\n    public function checkGroup(Notification $notification, array $group, string $operator = 'any'): bool\n    {\n        /** @var Collection<int, array> $children */\n        $children = collect($group['children'] ?? []); // @phpstan-ignore-line\n\n        if ($children->isEmpty()) {\n            return true;\n        }\n\n        $applies = true;\n\n        foreach ($children as $condition) {\n            if ($condition['type'] == 'condition') {\n                if (! NotificationRegistry::hasCondition(get_class($notification), $condition['condition'])) {\n                    continue;\n                }\n\n                /** @var Condition $instance */\n                $instance = app($condition['condition']);\n\n                $applies = $instance->applies(\n                    $notification,\n                    $condition['operand'] ?? null,\n                    $condition['operator'] ?? null,\n                    $condition['value'] ?? null,\n                    $condition['meta'] ?? null\n                );\n            }\n\n            if ($condition['type'] == 'group') {\n                $applies = $this->checkGroup($notification, $condition, $condition['operator']);\n            }\n\n            if ($operator === 'any' && $applies) {\n                return true;\n            }\n\n            if ($operator === 'all' && ! $applies) {\n                return false;\n            }\n        }\n\n        return $applies;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Conditions/FalseCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Conditions;\n\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass FalseCondition extends Condition\n{\n    public static string $name = 'FALSE';\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        return true;\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '!=' => 'Not equal to',\n        ];\n    }\n\n    public function operands(): array\n    {\n        return [\n            'operand-a' => 'Operand A',\n            'operand-b' => 'Operand B',\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Conditions/SelectCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Conditions;\n\nuse Vigilant\\Notifications\\Enums\\ConditionType;\n\nabstract class SelectCondition extends Condition\n{\n    public ConditionType $type = ConditionType::Select;\n\n    /** @return array<int|string, int|string> */\n    abstract public function options(): array;\n}\n"
  },
  {
    "path": "packages/notifications/src/Conditions/StaticCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Conditions;\n\nuse Vigilant\\Notifications\\Enums\\ConditionType;\n\nabstract class StaticCondition extends Condition\n{\n    public ConditionType $type = ConditionType::Static;\n\n    public function operators(): array\n    {\n        return [];\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Conditions/TrueCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Conditions;\n\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass TrueCondition extends Condition\n{\n    public static string $name = 'TRUE';\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        return true;\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '!=' => 'Not equal to',\n        ];\n    }\n\n    public function operands(): array\n    {\n        return [\n            'operand-a' => 'Operand A',\n            'operand-b' => 'Operand B',\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Contracts/HasSite.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Contracts;\n\nuse Vigilant\\Sites\\Models\\Site;\n\ninterface HasSite\n{\n    public function site(): ?Site;\n}\n"
  },
  {
    "path": "packages/notifications/src/Enums/ConditionType.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Enums;\n\nenum ConditionType: string\n{\n    case Text = 'text';\n    case Number = 'number';\n    case Select = 'select';\n    case Static = 'static';\n\n    public function view(): string\n    {\n        return 'notifications::condition-builder.type.'.$this->value;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Enums/Level.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Enums;\n\nenum Level: string\n{\n    case Info = 'info';\n    case Success = 'success';\n    case Warning = 'warning';\n    case Critical = 'critical';\n\n    public function color(): int\n    {\n        return match ($this) {\n            Level::Info => 0x3498DB,    // Blue\n            Level::Warning => 0xF1C40F, // Yellow\n            Level::Critical => 0xE74C3C,   // Red\n            Level::Success => 0x2ECC71, // Green\n        };\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Facades/NotificationRegistry.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Facades;\n\nuse Illuminate\\Support\\Facades\\Facade;\nuse Vigilant\\Notifications\\Notifications\\NotificationRegistry as Registry;\n\n/**\n * @method static Registry registerNotification(string|array $notifications)\n * @method static Registry registerChannel(string|array $channel)\n * @method static Registry registerCondition(string $notification, string|array $condition)\n * @method static array<int, class-string<Notification>> notifications()\n * @method static array channels()\n * @method static array conditions(string $notification)\n * @method static bool hasCondition(string $notification, string $condition)\n * @method static void fake()\n */\nclass NotificationRegistry extends Facade\n{\n    protected static function getFacadeAccessor(): string\n    {\n        return Registry::class;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Controllers/ChannelController.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Controllers;\n\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\View\\View;\nuse Vigilant\\Notifications\\Models\\Channel;\n\nclass ChannelController extends Controller\n{\n    public function index(): View\n    {\n        /** @var view-string $view */\n        $view = 'notifications::channels';\n        $hasChannels = Channel::query()->exists();\n\n        return view($view, [\n            'hasChannels' => $hasChannels,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/ChannelForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Frontend\\Traits\\CanBeInline;\nuse Vigilant\\Notifications\\Http\\Livewire\\Forms\\CreateChannelForm;\nuse Vigilant\\Notifications\\Jobs\\SendNotificationJob;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Notifications\\TestNotification;\n\nclass ChannelForm extends Component\n{\n    use CanBeInline;\n    use DisplaysAlerts;\n\n    public CreateChannelForm $form;\n\n    #[Locked]\n    public ?string $settingsComponent = null;\n\n    #[Locked]\n    public bool $componentValidated = false;\n\n    #[Locked]\n    public Channel $channelModel;\n\n    public bool $testSent = false;\n\n    public function mount(?Channel $channel): void\n    {\n        if ($channel !== null) {\n            $this->channelModel = $channel;\n            $this->form->fill($channel->toArray());\n\n            if (class_exists($channel->channel)) {\n                $this->settingsComponent = $channel->channel::$component ?? null;\n                \n                // If prefilled with settings, mark as validated\n                if (!empty($channel->settings)) {\n                    $this->componentValidated = true;\n                }\n            }\n        } else {\n            $this->channelModel = new Channel();\n        }\n    }\n\n    public function updated(): void\n    {\n        if (blank($this->form->channel)) {\n            return;\n        }\n\n        $this->form->settings = [];\n\n        $this->settingsComponent = $this->form->channel::$component ?? null;\n    }\n\n    #[On('update-channel-settings')]\n    public function updateChannelSettings(array $settings): void\n    {\n        $this->form->settings = $settings;\n    }\n\n    #[On('update-channel-validated')]\n    public function updateChannelValidated(bool $validated): void\n    {\n        $this->componentValidated = $validated;\n    }\n\n    public function save(bool $redirect = true, bool $dispatchSaved = true): void\n    {\n        if (! $this->componentValidated && $this->settingsComponent !== null) {\n            return;\n        }\n\n        $this->validate();\n\n        $data = $this->form->all();\n        $data['name'] = blank($data['name'] ?? null) ? null : $data['name'];\n\n        if ($this->channelModel->exists) {\n            $this->channelModel->update($data);\n        } else {\n            $this->channelModel = Channel::query()->create($data);\n        }\n\n        if ($this->inline) {\n            if ($dispatchSaved) {\n                $this->dispatch('channel-saved', [\n                    'channel' => $this->channelModel,\n                ]);\n            }\n\n            return;\n        }\n\n        if ($redirect) {\n            $this->redirectRoute('notifications.channel.edit', ['channel' => $this->channelModel]);\n\n            $this->alert(\n                __('Saved'),\n                __('Channel was successfully :action',\n                    ['action' => $this->channelModel->wasRecentlyCreated ? 'created' : 'saved']),\n                AlertType::Success\n            );\n        }\n    }\n\n    public function test(): void\n    {\n        $this->save(false, false);\n\n        if (! $this->channelModel->exists) {\n            return;\n        }\n\n        SendNotificationJob::dispatchSync(\n            new TestNotification,\n            $this->channelModel->team_id,\n            $this->channelModel->id\n        );\n\n        $this->testSent = true;\n    }\n\n    public function delete(): void\n    {\n        if (! $this->channelModel->exists) {\n            return;\n        }\n\n        $this->channelModel->delete();\n\n        $this->alert(\n            __('Deleted'),\n            __('Channel was successfully deleted'),\n            AlertType::Success\n        );\n\n        $this->redirectRoute('notifications.channels');\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'notifications::livewire.channels.form';\n\n        return view($view, [\n            'updating' => $this->channelModel->exists,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Channels/Configuration/ChannelConfiguration.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration;\n\nuse Illuminate\\View\\View;\nuse Livewire\\Component;\nuse Vigilant\\Notifications\\Channels\\NotificationChannel;\n\nabstract class ChannelConfiguration extends Component\n{\n    public string $channel;\n\n    public array $settings = [];\n\n    public array $rules = [];\n\n    public function mount(string $channel, array $settings = []): void\n    {\n        $this->channel = $channel;\n        $this->settings = $settings;\n    }\n\n    public function updated(): void\n    {\n        $this->dispatch('update-channel-validated', false);\n\n        /** @var NotificationChannel $channel */\n        $channel = app($this->channel);\n\n        $this->rules = collect($channel->rules())\n            ->mapWithKeys(fn (string|array $rules, string $key) => [\"settings.$key\" => $rules])\n            ->toArray();\n\n        $this->validate();\n\n        $this->dispatch('update-channel-settings', $this->settings);\n        $this->dispatch('update-channel-validated', true);\n    }\n\n    abstract public function render(): View;\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Channels/Configuration/Discord.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration;\n\nuse Illuminate\\View\\View;\n\nclass Discord extends ChannelConfiguration\n{\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'notifications::livewire.channels.configuration.discord';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Channels/Configuration/GoogleChat.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration;\n\nuse Illuminate\\View\\View;\n\nclass GoogleChat extends ChannelConfiguration\n{\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'notifications::livewire.channels.configuration.google-chat';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Channels/Configuration/Mail.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration;\n\nuse Illuminate\\View\\View;\n\nclass Mail extends ChannelConfiguration\n{\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'notifications::livewire.channels.configuration.mail';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Channels/Configuration/MicrosoftTeams.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration;\n\nuse Illuminate\\View\\View;\n\nclass MicrosoftTeams extends ChannelConfiguration\n{\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'notifications::livewire.channels.configuration.microsoft-teams';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Channels/Configuration/Ntfy.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration;\n\nuse Illuminate\\View\\View;\n\nclass Ntfy extends ChannelConfiguration\n{\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'notifications::livewire.channels.configuration.ntfy';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Channels/Configuration/Slack.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration;\n\nuse Illuminate\\View\\View;\n\nclass Slack extends ChannelConfiguration\n{\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'notifications::livewire.channels.configuration.slack';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Channels/Configuration/Telegram.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration;\n\nuse Illuminate\\View\\View;\n\nclass Telegram extends ChannelConfiguration\n{\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'notifications::livewire.channels.configuration.telegram';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Channels/Configuration/Webhook.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration;\n\nuse Illuminate\\View\\View;\n\nclass Webhook extends ChannelConfiguration\n{\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'notifications::livewire.channels.configuration.webhook';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Forms/CreateChannelForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Form;\n\nclass CreateChannelForm extends Form\n{\n    #[Validate('required|max:255')]\n    public string $channel = '';\n\n    #[Validate('nullable|string|max:255')]\n    public ?string $name = null;\n\n    public array $settings = [];\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Forms/CreateNotificationForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Form;\n\nclass CreateNotificationForm extends Form\n{\n    public bool $enabled = true;\n\n    #[Validate('required|max:255')]\n    public string $name = '';\n\n    #[Validate('required|max:255')]\n    public string $notification = '';\n\n    #[Validate('array')]\n    public array $conditions = [];\n\n    public ?int $cooldown = null;\n\n    public bool $all_channels = false;\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/NotificationForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Notifications\\Http\\Livewire\\Forms\\CreateNotificationForm;\nuse Vigilant\\Notifications\\Models\\Trigger;\n\nclass NotificationForm extends Component\n{\n    use DisplaysAlerts;\n\n    public CreateNotificationForm $form;\n\n    public array $channels = [];\n\n    #[Locked]\n    public Trigger $trigger;\n\n    public function mount(?Trigger $trigger): void\n    {\n        if ($trigger !== null) {\n            $this->trigger = $trigger;\n            $this->form->fill($trigger->toArray());\n            if ($trigger->exists) {\n                $this->channels = $trigger->channels->pluck('id')->toArray();\n            }\n        }\n    }\n\n    #[On('conditions-updated')]\n    public function conditionsUpdated(array $conditions): void\n    {\n        $this->form->conditions = $conditions;\n    }\n\n    public function save(): void\n    {\n        $this->validate();\n\n        if ($this->trigger->exists) {\n            // Never update the notification because of the linked conditions\n            $this->trigger->update($this->form->except(['notification']));\n        } else {\n            $this->trigger = Trigger::query()->create(\n                $this->form->all()\n            );\n        }\n\n        $this->trigger->channels()->sync($this->channels);\n\n        $this->alert(\n            __('Saved'),\n            __('Notification was successfully :action', ['action' => $this->trigger->wasRecentlyCreated ? 'created' : 'saved']),\n            AlertType::Success\n        );\n\n        $this->redirectRoute('notifications.trigger.edit', ['trigger' => $this->trigger]);\n    }\n\n    public function delete(): void\n    {\n        if (! $this->trigger->exists) {\n            return;\n        }\n\n        $this->trigger->delete();\n\n        $this->alert(\n            __('Deleted'),\n            __('Notification trigger was successfully deleted'),\n            AlertType::Success\n        );\n\n        $this->redirectRoute('notifications');\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'notifications::livewire.notifications.form';\n\n        return view($view, [\n            'updating' => $this->trigger->exists,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Notifications/Conditions/ConditionBuilder.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Notifications\\Conditions;\n\nuse Illuminate\\Support\\Arr;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\n\nclass ConditionBuilder extends Component\n{\n    #[Locked]\n    public string $notification;\n\n    public array $parent = [\n        'type' => 'group',\n        'operator' => 'any',\n    ];\n\n    public array $children = [];\n\n    public function mount(string $notification, array $initial = []): void\n    {\n        $this->notification = $notification;\n\n        if ($initial === []) {\n            return;\n        }\n\n        $this->parent['operator'] = $initial['operator'] ?? 'any';\n        $this->children = $initial['children'] ?? [];\n    }\n\n    public array $selectedCondition = [];\n\n    public function addCondition(string $path): void\n    {\n        $condition = $this->selectedCondition[md5($path)] ?? Arr::first($this->conditions());\n\n        $this->addToPath($path, [\n            'type' => 'condition',\n            'condition' => $condition,\n            'value' => null,\n        ]);\n    }\n\n    public function addGroup(string $path): void\n    {\n        $this->addToPath($path, [\n            'type' => 'group',\n            'operator' => 'all',\n            'children' => [],\n        ]);\n    }\n\n    public function deletePath(string $path): void\n    {\n        Arr::forget($this->children, $path);\n        $this->children = array_values($this->children);\n\n        $this->updated();\n    }\n\n    protected function addToPath(string $path, array $item): void\n    {\n        if (blank($path)) {\n            $this->children[] = $item;\n        } else {\n            $children = data_get($this->children, $path.'.children', []);\n            $children[] = $item;\n            data_set($this->children, $path.'.children', $children);\n        }\n\n        $this->updated();\n    }\n\n    public function updated(): void\n    {\n        $conditions = $this->parent;\n        $conditions['children'] = $this->children;\n\n        $this->dispatch('conditions-updated', $conditions);\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'notifications::livewire.notifications.condition-builder';\n\n        return view($view, [\n            'conditions' => $this->conditions(),\n        ]);\n    }\n\n    protected function conditions(): array\n    {\n        return NotificationRegistry::conditions($this->notification);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Tables/ChannelTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Query\\Builder as QueryBuilder;\nuse Illuminate\\Support\\Enumerable;\nuse Illuminate\\Support\\Facades\\DB;\nuse RamonRietdijk\\LivewireTables\\Actions\\Action;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse RamonRietdijk\\LivewireTables\\Enums\\Direction;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Notifications\\Channels\\NotificationChannel;\nuse Vigilant\\Notifications\\Models\\Channel;\n\nclass ChannelTable extends BaseTable\n{\n    protected string $model = Channel::class;\n\n    protected function columns(): array\n    {\n        return [\n            Column::make(__('Name'), 'name')\n                ->displayUsing(function (?string $name, Channel $channel): string {\n                    return $channel->title();\n                }),\n\n            Column::make(__('Channel Type'), 'channel')\n                ->displayUsing(function (string $channel) {\n                    /** @var class-string<NotificationChannel> $channel */\n\n                    return $channel::$name;\n                }),\n\n            Column::make(__('Notifications Sent'), 'total_notification_history')\n                ->sortable(function (Builder $builder, Direction $direction): void {\n                    $builder->orderBy(function (QueryBuilder $query): void {\n                        $query->selectRaw('COUNT(*)')->from('notification_history')->where('channel_id', '=', DB::raw('notification_channels.id'));\n                    }, $direction->value);\n                }),\n        ];\n    }\n\n    protected function actions(): array\n    {\n        return [\n            Action::make(__('Delete'), function (Enumerable $models): void {\n                $models->each(fn (Channel $channel) => $channel->delete());\n            }, 'delete'),\n        ];\n    }\n\n    public function link(Model $model): ?string\n    {\n        return route('notifications.channel.edit', ['channel' => $model]);\n    }\n\n    protected function applySelect(Builder $builder): static\n    {\n        parent::applySelect($builder);\n\n        $builder->addSelect(\n            DB::raw('(SELECT COUNT(`notification_history`.`id`) FROM `notification_history` WHERE `notification_history`.`channel_id` = `notification_channels`.`id` GROUP BY `notification_history`.`channel_id`) AS total_notification_history')\n        );\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Tables/HistoryTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse RamonRietdijk\\LivewireTables\\Filters\\SelectFilter;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\DateColumn;\nuse Vigilant\\Notifications\\Channels\\NotificationChannel;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Models\\History;\n\nclass HistoryTable extends BaseTable\n{\n    protected string $model = History::class;\n\n    public string $sortColumn = 'created_at';\n\n    public string $sortDirection = 'desc';\n\n    protected function columns(): array\n    {\n        return [\n            Column::make(__('Type'), 'trigger.name')\n                ->searchable(),\n\n            Column::make(__('Channel'), 'channel.channel')\n                ->searchable()\n                ->displayUsing(function (?string $channel) {\n                    if ($channel === null) {\n                        return null;\n                    }\n\n                    /** @var class-string<NotificationChannel> $channel */\n                    return $channel::$name;\n                }),\n\n            Column::make(__('Level'), 'data.level')\n                ->displayUsing(fn (string $level) => Level::tryFrom($level)->name ?? $level),\n\n            Column::make(__('Notification'), 'data.title')\n                ->searchable(function (Builder $builder, mixed $search) {\n                    $builder->where('data->title', 'LIKE', '%'.$search.'%');\n                }),\n\n            Column::make(__('Details'), 'data.description')\n                ->displayUsing(fn (string $description): string => str($description)->limit(100))\n                ->searchable(function (Builder $builder, mixed $search) {\n                    $builder->where('data->description', 'LIKE', '%'.$search.'%');\n                }),\n\n            DateColumn::make(__('Notified At'), 'created_at')\n                ->sortable(),\n        ];\n    }\n\n    protected function filters(): array\n    {\n        return [\n            SelectFilter::make(__('Level'), 'data->level')\n                ->options(\n                    collect(Level::cases())\n                        ->mapWithKeys(fn (Level $level) => [$level->value => $level->name])\n                        ->toArray()\n                ),\n\n            SelectFilter::make(__('Channel'), 'channel_id')\n                ->options(\n                    Channel::query()\n                        ->select(['id', 'channel'])\n                        ->get()\n                        ->mapwithKeys(function (Channel $channel): array {\n                            /** @var class-string<NotificationChannel> $class */\n                            $class = $channel->channel;\n\n                            return [$channel->id => $class::$name];\n                        })\n                        ->toArray()\n                ),\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Http/Livewire/Tables/NotificationTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Http\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Query\\Builder as QueryBuilder;\nuse Illuminate\\Support\\Enumerable;\nuse Illuminate\\Support\\Facades\\DB;\nuse RamonRietdijk\\LivewireTables\\Actions\\Action;\nuse RamonRietdijk\\LivewireTables\\Columns\\BooleanColumn;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse RamonRietdijk\\LivewireTables\\Enums\\Direction;\nuse RamonRietdijk\\LivewireTables\\Filters\\BooleanFilter;\nuse RamonRietdijk\\LivewireTables\\Filters\\SelectFilter;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Notifications\\Models\\Trigger;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass NotificationTable extends BaseTable\n{\n    protected string $model = Trigger::class;\n\n    protected function columns(): array\n    {\n        return [\n            BooleanColumn::make(__('Enabled'), 'enabled')\n                ->sortable(),\n\n            Column::make(__('Name'), 'name')\n                ->sortable()\n                ->searchable(),\n\n            Column::make(__('Type'), 'notification')\n                ->displayUsing(function (string $notification) {\n\n                    if (! class_exists($notification)) {\n                        return $notification;\n                    }\n\n                    /** @var class-string<Notification> $notification */\n                    return $notification::$name;\n                }),\n\n            Column::make(__('Channels'))\n                ->displayUsing(function (Trigger $trigger) {\n                    return $trigger->all_channels\n                        ? __('All Channels')\n                        : __(':count Channel(s)', ['count' => $trigger->channels()->count()]);\n                }),\n\n            Column::make(__('Notifications Sent'), 'total_notification_history')\n                ->sortable(function (Builder $builder, Direction $direction): void {\n                    $builder->orderBy(function (QueryBuilder $query): void {\n                        $query->selectRaw('COUNT(*)')->from('notification_history')->where('trigger_id', '=', DB::raw('notification_triggers.id'));\n                    }, $direction->value);\n                }),\n        ];\n    }\n\n    protected function filters(): array\n    {\n        return [\n            BooleanFilter::make(__('Enabled'), 'enabled'),\n            SelectFilter::make(__('Type'), 'notification')\n                ->options(\n                    collect(NotificationRegistry::notifications())\n                        ->mapWithKeys(fn (string $notification): array => [$notification => $notification::$name]) // @phpstan-ignore-line\n                        ->toArray()\n                ),\n        ];\n    }\n\n    protected function actions(): array\n    {\n        return [\n            Action::make(__('Enable'), function (Enumerable $models): void {\n                Trigger::query()\n                    ->whereIn('id', $models->pluck('id'))\n                    ->update([\n                        'enabled' => true,\n                    ]);\n            }, 'enable'),\n\n            Action::make(__('Disable'), function (Enumerable $models): void {\n                Trigger::query()\n                    ->whereIn('id', $models->pluck('id'))\n                    ->update([\n                        'enabled' => false,\n                    ]);\n            }, 'disable'),\n\n            Action::make(__('Delete'), function (Enumerable $models): void {\n                $models->each(fn (Trigger $trigger) => $trigger->delete());\n            }, 'delete'),\n\n        ];\n    }\n\n    public function link(Model $model): ?string\n    {\n        return route('notifications.trigger.edit', ['trigger' => $model]);\n    }\n\n    protected function applySelect(Builder $builder): static\n    {\n        parent::applySelect($builder);\n\n        $builder->addSelect(\n            DB::raw('(SELECT COUNT(`notification_history`.`id`) FROM `notification_history` WHERE `notification_history`.`trigger_id` = `notification_triggers`.`id` GROUP BY `notification_history`.`trigger_id`) AS total_notification_history')\n        );\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Jobs/CreateNotificationsJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Notifications\\Actions\\CreateNotifications;\nuse Vigilant\\Users\\Models\\Team;\n\nclass CreateNotificationsJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(\n        public Team $team\n    ) {\n        $this->onQueue(config('notifications.queue'));\n    }\n\n    public function handle(CreateNotifications $notifications, TeamService $teamService): void\n    {\n        $teamService->setTeam($this->team);\n        $notifications->create($this->team);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->team->id;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Jobs/SendNotificationJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Notifications\\Channels\\NotificationChannel;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass SendNotificationJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n\n    public function __construct(\n        public Notification $notification,\n        public int $teamId,\n        public int $channelId,\n        public ?int $triggerId = null,\n    ) {\n        $this->onQueue(config('notifications.queue'));\n    }\n\n    public function handle(TeamService $teamService): void\n    {\n        $teamService->setTeamById($this->teamId);\n\n        /** @var Channel $channel */\n        $channel = Channel::query()->findOrFail($this->channelId);\n\n        /** @var NotificationChannel $instance */\n        $instance = app($channel->channel);\n\n        $instance->fire($this->notification, $channel);\n\n        $channel->history()->create([\n            'trigger_id' => $this->triggerId,\n            'notification' => get_class($this->notification),\n            'uniqueId' => $this->notification->uniqueId(),\n            'data' => $this->notification->toArray(),\n        ]);\n    }\n\n    public function uniqueId(): string\n    {\n        return implode('-', [\n            get_class($this->notification),\n            $this->channelId,\n            $this->triggerId ?? 0,\n            $this->notification->uniqueId(),\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Mail/NotificationMail.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Mail;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Mail\\Mailable;\nuse Illuminate\\Mail\\Mailables\\Content;\nuse Illuminate\\Mail\\Mailables\\Envelope;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass NotificationMail extends Mailable\n{\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(public Notification $notification) {}\n\n    public function envelope(): Envelope\n    {\n        return new Envelope(\n            subject: implode(' - ', [\n                $this->notification->level()->name,\n                $this->notification->title(),\n                'Vigilant',\n            ])\n        );\n    }\n\n    public function content(): Content\n    {\n        return new Content(\n            view: 'notifications::mails.notification',\n            with: [\n                'description' => $this->notification->description(),\n                'viewUrl' => $this->notification->viewUrl(),\n                'url' => $this->notification->url(),\n                'urlTitle' => $this->notification->urlTitle(),\n            ],\n        );\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Models/Channel.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Notifications\\Channels\\NotificationChannel;\nuse Vigilant\\Notifications\\Observers\\ChannelObserver;\n\n/**\n * @property int $id\n * @property int $team_id\n * @property string $channel\n * @property string|null $name\n * @property array $settings\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property Collection<int, History> $history\n */\n#[ObservedBy([ChannelObserver::class])]\n#[ScopedBy([TeamScope::class])]\nclass Channel extends Model\n{\n    protected $table = 'notification_channels';\n\n    protected $guarded = [];\n\n    protected $casts = [\n        'settings' => 'array',\n    ];\n\n    public function title(): string\n    {\n        if (filled($this->name)) {\n            return $this->name;\n        }\n\n        if (is_string($this->channel) && class_exists($this->channel) && is_subclass_of($this->channel, NotificationChannel::class)) {\n            /** @var class-string<NotificationChannel> $channel */\n            $channel = $this->channel;\n\n            return $channel::$name;\n        }\n\n        return (string) $this->channel;\n    }\n\n    public function history(): HasMany\n    {\n        return $this->hasMany(History::class);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Models/History.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Prunable;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Concerns\\HasDataRetention;\nuse Vigilant\\Notifications\\Scopes\\HistoryTeamScope;\n\n/**\n * @property int $id\n * @property int $trigger_id\n * @property int $channel_id\n * @property string $notification\n * @property string $uniqueId\n * @property array $data\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Trigger $trigger\n * @property ?Channel $channel\n */\n#[ScopedBy([HistoryTeamScope::class])]\nclass History extends Model\n{\n    use HasDataRetention;\n    use Prunable;\n\n    protected $table = 'notification_history';\n\n    protected $guarded = [];\n\n    protected $casts = [\n        'data' => 'array',\n    ];\n\n    public function trigger(): BelongsTo\n    {\n        return $this->belongsTo(Trigger::class);\n    }\n\n    public function channel(): BelongsTo\n    {\n        return $this->belongsTo(Channel::class);\n    }\n\n    public function prunable(): Builder\n    {\n        return static::withoutGlobalScopes()->where('created_at', '<=', $this->retentionPeriod());\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Models/Trigger.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Notifications\\Observers\\TriggerObserver;\nuse Vigilant\\Users\\Models\\Team;\n\n/**\n * @property int $id\n * @property int $team_id\n * @property bool $enabled\n * @property string $notification\n * @property string $name\n * @property array $conditions\n * @property ?int $cooldown\n * @property bool $all_channels\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Team $team\n * @property Collection<int, Channel> $channels\n * @property Collection<int, History> $history\n */\n#[ObservedBy([TriggerObserver::class])]\n#[ScopedBy([TeamScope::class])]\nclass Trigger extends Model\n{\n    protected $table = 'notification_triggers';\n\n    protected $guarded = [];\n\n    protected $casts = [\n        'enabled' => 'bool',\n        'conditions' => 'array',\n        'all_channels' => 'bool',\n    ];\n\n    public function team(): BelongsTo\n    {\n        return $this->belongsTo(Team::class);\n    }\n\n    public function channels(): BelongsToMany\n    {\n        return $this->belongsToMany(Channel::class, 'notification_channel_notification_trigger');\n    }\n\n    public function history(): HasMany\n    {\n        return $this->hasMany(History::class);\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Notifications/Notification.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Notifications;\n\nuse Illuminate\\Contracts\\Support\\Arrayable;\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Notifications\\Actions\\CheckBurst;\nuse Vigilant\\Notifications\\Actions\\CheckCooldown;\nuse Vigilant\\Notifications\\Concerns\\NotificationFake;\nuse Vigilant\\Notifications\\Conditions\\ConditionEngine;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Jobs\\SendNotificationJob;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Models\\Trigger;\n\nabstract class Notification implements Arrayable\n{\n    use NotificationFake;\n\n    public static string $name = '';\n\n    public string $title = '';\n\n    public string $description = '';\n\n    public Level $level = Level::Info;\n\n    public static ?int $defaultCooldown = null;\n\n    public static array $defaultConditions = [];\n\n    public static bool $autoCreate = true;\n\n    public static function make(mixed ...$args): static\n    {\n        return new static(...$args);\n    }\n\n    public static function notify(mixed ...$args): void\n    {\n        $instance = new static(...$args);\n\n        if (static::$faked) {\n            static::$fakeDispatches[] = $instance;\n\n            return;\n        }\n\n        /** @var Collection<int, Trigger> $triggers */\n        $triggers = Trigger::query()\n            ->with('channels')\n            ->where('enabled', '=', true)\n            ->where('notification', '=', static::class)\n            ->get();\n\n        /** @var ConditionEngine $conditionEngine */\n        $conditionEngine = app(ConditionEngine::class);\n\n        /** @var CheckCooldown $cooldownCheck */\n        $cooldownCheck = app(CheckCooldown::class);\n\n        /** @var CheckBurst $burstCheck */\n        $burstCheck = app(CheckBurst::class);\n\n        foreach ($triggers as $trigger) {\n\n            if (! $conditionEngine->checkGroup($instance, $trigger->conditions,\n                $trigger->conditions['operator'] ?? 'any')) {\n                continue;\n            }\n\n            $channels = $trigger->all_channels ? Channel::all() : $trigger->channels;\n\n            foreach ($channels as $channel) {\n\n                if ($cooldownCheck->onCooldown($trigger, $channel, $instance)) {\n                    continue;\n                }\n\n                if ($burstCheck->isBursting($instance, $trigger, $channel)) {\n                    return;\n                }\n\n                SendNotificationJob::dispatch($instance, $channel->team_id, $channel->id, $trigger->id);\n            }\n        }\n    }\n\n    abstract public function uniqueId(): string|int;\n\n    public function level(): Level\n    {\n        return $this->level;\n    }\n\n    public function title(): string\n    {\n        return $this->title;\n    }\n\n    public function description(): string\n    {\n        return $this->description;\n    }\n\n    public static function info(): ?string\n    {\n        return null;\n    }\n\n    public function viewUrl(): ?string\n    {\n        return null;\n    }\n\n    public function url(): ?string\n    {\n        return null;\n    }\n\n    public function urlTitle(): ?string\n    {\n        return null;\n    }\n\n    public function toArray(): array\n    {\n        return [\n            'title' => $this->title(),\n            'description' => $this->description(),\n            'level' => $this->level(),\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Notifications/NotificationRegistry.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Notifications;\n\nuse Illuminate\\Support\\Arr;\nuse Vigilant\\Notifications\\Channels\\NotificationChannel;\nuse Vigilant\\Notifications\\Conditions\\Condition;\n\nclass NotificationRegistry\n{\n    protected array $notifications = [];\n\n    protected array $conditions = [];\n\n    protected array $channels = [];\n\n    /**\n     * @param  class-string<Notification>|array<int, class-string<Notification>>  $notification\n     */\n    public function registerNotification(string|array $notification): static\n    {\n        $this->notifications = array_merge($this->notifications(), Arr::wrap($notification));\n\n        return $this;\n    }\n\n    /**\n     * @param  class-string<Condition>|array<int, class-string<Condition>>  $condition\n     */\n    public function registerCondition(string $notification, string|array $condition): static\n    {\n        $this->conditions[$notification] = array_merge($this->conditions($notification), Arr::wrap($condition));\n\n        return $this;\n    }\n\n    /**\n     * @param  class-string<NotificationChannel>|array<int, class-string<NotificationChannel>>  $channel\n     */\n    public function registerChannel(string|array $channel): static\n    {\n        $this->channels = array_merge($this->channels(), Arr::wrap($channel));\n\n        return $this;\n    }\n\n    public function notifications(): array\n    {\n        return $this->notifications;\n    }\n\n    public function channels(): array\n    {\n        return $this->channels;\n    }\n\n    public function conditions(string $notification): array\n    {\n        return $this->conditions[$notification] ?? [];\n    }\n\n    public function hasCondition(string $notification, string $condition): bool\n    {\n        return in_array($condition, $this->conditions($notification));\n    }\n\n    public function fake(): void\n    {\n        $this->notifications = [];\n        $this->channels = [];\n        $this->conditions = [];\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Notifications/TestNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Notifications;\n\nuse Vigilant\\Notifications\\Enums\\Level;\n\nclass TestNotification extends Notification\n{\n    public static string $name = 'Test Notification';\n\n    public string $description = 'If you receive this notification it means that the notification channel is working!';\n\n    public function __construct(public Level $level = Level::Success) {}\n\n    public function title(): string\n    {\n        return 'Test Notification for level '.$this->level->name;\n    }\n\n    public function viewUrl(): ?string\n    {\n        return route('notifications');\n    }\n\n    public function url(): ?string\n    {\n        return route('notifications');\n    }\n\n    public function urlTitle(): ?string\n    {\n        return __('Details');\n    }\n\n    /** @codeCoverageIgnore */\n    public function uniqueId(): string\n    {\n        return $this->level->value;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Observers/ChannelObserver.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Observers;\n\nuse Illuminate\\Support\\Facades\\Auth;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Users\\Models\\User;\n\nclass ChannelObserver\n{\n    public function creating(Channel $channel): void\n    {\n        /** @var ?User $user */\n        $user = Auth::user();\n\n        if ($user !== null && $user->currentTeam !== null) {\n            $channel->team_id = $user->currentTeam->id;\n        }\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Observers/TriggerObserver.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Observers;\n\nuse Illuminate\\Support\\Facades\\Auth;\nuse Vigilant\\Notifications\\Models\\Trigger;\nuse Vigilant\\Users\\Models\\User;\n\nclass TriggerObserver\n{\n    public function creating(Trigger $trigger): void\n    {\n        if ($trigger->team_id === null) {\n            /** @var ?User $user */\n            $user = Auth::user();\n\n            if ($user !== null && $user->currentTeam !== null) {\n                $trigger->team_id = $user->currentTeam->id;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/Scopes/HistoryTeamScope.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Scopes;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Scope;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Notifications\\Models\\History;\n\nclass HistoryTeamScope implements Scope\n{\n    public function apply(Builder $builder, Model $model): void\n    {\n        /** @var History $model */\n        /** @var TeamService $teamService */\n        $teamService = app(TeamService::class);\n        $team = $teamService->team();\n\n        $builder->whereHas('channel', function (Builder $query) use ($team) {\n            $query->where($query->qualifyColumn('team_id'), '=', $team->id);\n        });\n    }\n}\n"
  },
  {
    "path": "packages/notifications/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications;\n\nuse Illuminate\\Foundation\\Bus\\PendingDispatch;\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\nuse Livewire\\Livewire;\nuse Vigilant\\Core\\Facades\\Navigation;\nuse Vigilant\\Core\\Policies\\AllowAllPolicy;\nuse Vigilant\\Notifications\\Channels\\DiscordChannel;\nuse Vigilant\\Notifications\\Channels\\GoogleChatChannel;\nuse Vigilant\\Notifications\\Channels\\MailChannel;\nuse Vigilant\\Notifications\\Channels\\MicrosoftTeamsChannel;\nuse Vigilant\\Notifications\\Channels\\NtfyChannel;\nuse Vigilant\\Notifications\\Channels\\SlackChannel;\nuse Vigilant\\Notifications\\Channels\\TelegramChannel;\nuse Vigilant\\Notifications\\Commands\\CreateNotificationsCommand;\nuse Vigilant\\Notifications\\Commands\\RenameConditionClassesCommand;\nuse Vigilant\\Notifications\\Commands\\TestNotificationCommand;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Notifications\\Http\\Livewire\\ChannelForm;\nuse Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration\\Discord;\nuse Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration\\GoogleChat;\nuse Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration\\Mail;\nuse Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration\\MicrosoftTeams;\nuse Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration\\Ntfy;\nuse Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration\\Slack;\nuse Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration\\Telegram;\nuse Vigilant\\Notifications\\Http\\Livewire\\Channels\\Configuration\\Webhook;\nuse Vigilant\\Notifications\\Http\\Livewire\\NotificationForm;\nuse Vigilant\\Notifications\\Http\\Livewire\\Notifications;\nuse Vigilant\\Notifications\\Http\\Livewire\\Tables\\ChannelTable;\nuse Vigilant\\Notifications\\Http\\Livewire\\Tables\\HistoryTable;\nuse Vigilant\\Notifications\\Http\\Livewire\\Tables\\NotificationTable;\nuse Vigilant\\Notifications\\Jobs\\CreateNotificationsJob;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Models\\Trigger;\nuse Vigilant\\Users\\Models\\Team;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function register(): void\n    {\n        $this\n            ->registerConfig()\n            ->registerSingletons();\n    }\n\n    protected function registerConfig(): static\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/notifications.php', 'notifications');\n\n        return $this;\n    }\n\n    protected function registerSingletons(): static\n    {\n        $this->app->singleton(\\Vigilant\\Notifications\\Notifications\\NotificationRegistry::class);\n\n        return $this;\n    }\n\n    public function boot(): void\n    {\n        $this\n            ->bootConfig()\n            ->bootMigrations()\n            ->bootCommands()\n            ->bootViews()\n            ->bootEvents()\n            ->bootLivewire()\n            ->bootRoutes()\n            ->bootNavigation()\n            ->bootNotificationChannels()\n            ->bootPolicies();\n    }\n\n    protected function bootConfig(): static\n    {\n        $this->publishes([\n            __DIR__.'/../config/notifications.php' => config_path('notifications.php'),\n        ], 'config');\n\n        return $this;\n    }\n\n    protected function bootMigrations(): static\n    {\n        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');\n\n        return $this;\n    }\n\n    protected function bootCommands(): static\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n                CreateNotificationsCommand::class,\n                RenameConditionClassesCommand::class,\n                TestNotificationCommand::class,\n            ]);\n        }\n\n        return $this;\n    }\n\n    protected function bootViews(): static\n    {\n        $this->loadViewsFrom(__DIR__.'/../resources/views', 'notifications');\n\n        return $this;\n    }\n\n    protected function bootEvents(): static\n    {\n        Team::created(fn (Team $team): PendingDispatch => CreateNotificationsJob::dispatch($team));\n\n        return $this;\n    }\n\n    protected function bootLivewire(): static\n    {\n        Livewire::component('notification-table', NotificationTable::class);\n        Livewire::component('notification-form', NotificationForm::class);\n\n        Livewire::component('notification-history-table', HistoryTable::class);\n\n        Livewire::component('notification-condition-builder', Notifications\\Conditions\\ConditionBuilder::class);\n\n        Livewire::component('channel-table', ChannelTable::class);\n        Livewire::component('channel-form', ChannelForm::class);\n\n        Livewire::component('channel-configuration-webhook', Webhook::class);\n        Livewire::component('channel-configuration-ntfy', Ntfy::class);\n        Livewire::component('channel-configuration-mail', Mail::class);\n        Livewire::component('channel-configuration-slack', Slack::class);\n        Livewire::component('channel-configuration-discord', Discord::class);\n        Livewire::component('channel-configuration-google-chat', GoogleChat::class);\n        Livewire::component('channel-configuration-microsoft-teams', MicrosoftTeams::class);\n        Livewire::component('channel-configuration-telegram', Telegram::class);\n\n        return $this;\n    }\n\n    protected function bootRoutes(): static\n    {\n        if (! $this->app->routesAreCached()) {\n            Route::middleware(['web', 'auth'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/web.php'));\n        }\n\n        return $this;\n    }\n\n    protected function bootNavigation(): static\n    {\n        Navigation::path(__DIR__.'/../resources/navigation.php');\n\n        return $this;\n    }\n\n    protected function bootNotificationChannels(): static\n    {\n        NotificationRegistry::registerChannel([\n            NtfyChannel::class,\n            MailChannel::class,\n            DiscordChannel::class,\n            SlackChannel::class,\n            GoogleChatChannel::class,\n            MicrosoftTeamsChannel::class,\n            TelegramChannel::class,\n        ]);\n\n        return $this;\n    }\n\n    protected function bootPolicies(): static\n    {\n        Gate::policy(Trigger::class, AllowAllPolicy::class);\n        Gate::policy(Channel::class, AllowAllPolicy::class);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/testbench.yaml",
    "content": "providers:\n  - Vigilant\\Notifications\\ServiceProvider\n"
  },
  {
    "path": "packages/notifications/tests/Channels/NtfyChannelTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Tests\\Channels;\n\nuse Illuminate\\Http\\Client\\Request;\nuse Illuminate\\Support\\Facades\\Http;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Notifications\\Channels\\NtfyChannel;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Tests\\Fakes\\FakeNotification;\nuse Vigilant\\Notifications\\Tests\\TestCase;\n\nclass NtfyChannelTest extends TestCase\n{\n    #[Test]\n    public function it_sends_to_ntfy(): void\n    {\n        config()->set('app.name', 'Vigilant');\n        Http::fake([\n            'ntfy/topic' => Http::response(),\n        ]);\n\n        Channel::withoutEvents(function () {\n            Channel::query()->create([\n                'team_id' => 1,\n                'channel' => NtfyChannel::class,\n                'settings' => [\n                    'server' => 'ntfy',\n                    'topic' => 'topic',\n                    'auth_method' => 'username',\n                    'username' => 'username',\n                    'password' => 'password',\n                ],\n            ]);\n        });\n\n        $notification = FakeNotification::make(1);\n        /** @var Channel $channelModel */\n        $channelModel = Channel::query()->withoutGlobalScopes()->first();\n\n        /** @var NtfyChannel $channel */\n        $channel = app(NtfyChannel::class);\n\n        $channel->fire($notification, $channelModel);\n\n        Http::assertSent(function (Request $request): bool {\n            return $request->header('Authorization') === ['Basic dXNlcm5hbWU6cGFzc3dvcmQ='] &&\n                $request->header('Title') === ['Title of this fake notification - Vigilant'] &&\n                $request->header('Tags') === ['triangular_flag_on_post'] &&\n                $request->body() === 'Description of this fake notification';\n        });\n    }\n}\n"
  },
  {
    "path": "packages/notifications/tests/Channels/TelegramChannelTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Tests\\Channels;\n\nuse Illuminate\\Http\\Client\\Request;\nuse Illuminate\\Support\\Facades\\Http;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Notifications\\Channels\\TelegramChannel;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Tests\\Fakes\\FakeNotification;\nuse Vigilant\\Notifications\\Tests\\TestCase;\n\nclass TelegramChannelTest extends TestCase\n{\n    #[Test]\n    public function it_sends_to_telegram(): void\n    {\n        Http::fake([\n            'api.telegram.org/*' => Http::response(),\n        ]);\n\n        Channel::withoutEvents(function () {\n            Channel::query()->create([\n                'team_id' => 1,\n                'channel' => TelegramChannel::class,\n                'settings' => [\n                    'bot_token' => 'test_bot_token',\n                    'chat_id' => '123456789',\n                ],\n            ]);\n        });\n\n        $notification = FakeNotification::make(1);\n        /** @var Channel $channelModel */\n        $channelModel = Channel::query()->withoutGlobalScopes()->first();\n\n        /** @var TelegramChannel $channel */\n        $channel = app(TelegramChannel::class);\n\n        $channel->fire($notification, $channelModel);\n\n        Http::assertSent(function (Request $request): bool {\n            $body = $request->data();\n\n            return $request->url() === 'https://api.telegram.org/bottest_bot_token/sendMessage' &&\n                $body['chat_id'] === '123456789' &&\n                $body['text'] === \"*Title of this fake notification*\\n\\nDescription of this fake notification\" &&\n                $body['parse_mode'] === 'MarkdownV2';\n        });\n    }\n\n    #[Test]\n    public function it_escapes_markdown_v2_special_characters(): void\n    {\n        Http::fake([\n            'api.telegram.org/*' => Http::response(),\n        ]);\n\n        Channel::withoutEvents(function () {\n            Channel::query()->create([\n                'team_id' => 1,\n                'channel' => TelegramChannel::class,\n                'settings' => [\n                    'bot_token' => 'test_bot_token',\n                    'chat_id' => '123456789',\n                ],\n            ]);\n        });\n\n        $notification = new class(1) extends FakeNotification\n        {\n            public string $title = 'Alert: CPU_usage > 90% [critical]';\n\n            public string $description = 'Host 192.168.1.1 is down. Check #monitoring.';\n        };\n\n        /** @var Channel $channelModel */\n        $channelModel = Channel::query()->withoutGlobalScopes()->first();\n\n        /** @var TelegramChannel $channel */\n        $channel = app(TelegramChannel::class);\n\n        $channel->fire($notification, $channelModel);\n\n        Http::assertSent(function (Request $request): bool {\n            $body = $request->data();\n\n            return $body['text'] === \"*Alert: CPU\\_usage \\> 90% \\[critical\\]*\\n\\nHost 192\\.168\\.1\\.1 is down\\. Check \\#monitoring\\.\" &&\n                $body['parse_mode'] === 'MarkdownV2';\n        });\n    }\n\n    #[Test]\n    public function it_sends_telegram_message_with_inline_keyboard(): void\n    {\n        Http::fake([\n            'api.telegram.org/*' => Http::response(),\n        ]);\n\n        Channel::withoutEvents(function () {\n            Channel::query()->create([\n                'team_id' => 1,\n                'channel' => TelegramChannel::class,\n                'settings' => [\n                    'bot_token' => 'test_bot_token',\n                    'chat_id' => '123456789',\n                ],\n            ]);\n        });\n\n        $notification = new class(1) extends FakeNotification\n        {\n            public function viewUrl(): string\n            {\n                return 'https://example.com/view';\n            }\n\n            public function url(): string\n            {\n                return 'https://example.com/action';\n            }\n\n            public function urlTitle(): string\n            {\n                return 'Take Action';\n            }\n        };\n\n        /** @var Channel $channelModel */\n        $channelModel = Channel::query()->withoutGlobalScopes()->first();\n\n        /** @var TelegramChannel $channel */\n        $channel = app(TelegramChannel::class);\n\n        $channel->fire($notification, $channelModel);\n\n        Http::assertSent(function (Request $request): bool {\n            $body = $request->data();\n\n            return $request->url() === 'https://api.telegram.org/bottest_bot_token/sendMessage' &&\n                isset($body['reply_markup']['inline_keyboard']) &&\n                count($body['reply_markup']['inline_keyboard']) === 2 &&\n                $body['reply_markup']['inline_keyboard'][0][0]['text'] === 'View in Vigilant' &&\n                $body['reply_markup']['inline_keyboard'][0][0]['url'] === 'https://example.com/view' &&\n                $body['reply_markup']['inline_keyboard'][1][0]['text'] === 'Take Action' &&\n                $body['reply_markup']['inline_keyboard'][1][0]['url'] === 'https://example.com/action';\n        });\n    }\n}\n"
  },
  {
    "path": "packages/notifications/tests/Conditions/ConditionEngineTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Tests\\Conditions;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Notifications\\Conditions\\ConditionEngine;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Notifications\\Tests\\Fakes\\Conditions\\FalseCondition;\nuse Vigilant\\Notifications\\Tests\\Fakes\\Conditions\\TrueCondition;\nuse Vigilant\\Notifications\\Tests\\Fakes\\FakeNotification;\nuse Vigilant\\Notifications\\Tests\\TestCase;\n\nclass ConditionEngineTest extends TestCase\n{\n    #[Test]\n    #[DataProvider('conditions')]\n    public function it_checks_conditions(array $groups, string $operator, bool $expected): void\n    {\n        NotificationRegistry::registerCondition(FakeNotification::class, [\n            TrueCondition::class,\n            FalseCondition::class,\n        ]);\n\n        /** @var ConditionEngine $engine */\n        $engine = app(ConditionEngine::class);\n\n        $this->assertEquals($expected, $engine->checkGroup(\n            FakeNotification::make(1),\n            $groups,\n            $operator\n        ));\n    }\n\n    public static function conditions(): array\n    {\n        return [\n            'No conditions' => [\n                'groups' => [],\n                'operator' => 'all',\n                'expected' => true,\n            ],\n\n            'True' => [\n                'groups' => [\n                    'type' => 'group',\n                    'children' => [\n                        [\n                            'type' => 'condition',\n                            'condition' => TrueCondition::class,\n                            'operator' => '=',\n\n                        ],\n                    ],\n                ],\n                'operator' => 'all',\n                'expected' => true,\n            ],\n\n            'False' => [\n                'groups' => [\n                    'type' => 'group',\n                    'children' => [\n                        [\n                            'type' => 'condition',\n                            'condition' => FalseCondition::class,\n                            'operator' => '=',\n\n                        ],\n                    ],\n                ],\n                'operator' => 'all',\n                'expected' => false,\n            ],\n\n            'Any' => [\n                'groups' => [\n                    'type' => 'group',\n                    'children' => [\n                        [\n                            'type' => 'condition',\n                            'condition' => FalseCondition::class,\n                            'operator' => '=',\n\n                        ],\n                        [\n                            'type' => 'condition',\n                            'condition' => TrueCondition::class,\n                            'operator' => '=',\n\n                        ],\n                    ],\n                ],\n                'operator' => 'any',\n                'expected' => true,\n            ],\n\n            'All' => [\n                'groups' => [\n                    'type' => 'group',\n                    'children' => [\n                        [\n                            'type' => 'condition',\n                            'condition' => TrueCondition::class,\n                            'operator' => '=',\n\n                        ],\n                        [\n                            'type' => 'condition',\n                            'condition' => FalseCondition::class,\n                            'operator' => '=',\n\n                        ],\n                    ],\n                ],\n                'operator' => 'all',\n                'expected' => false,\n            ],\n\n            'Children' => [\n                'groups' => [\n                    'type' => 'group',\n                    'children' => [\n                        [\n                            'type' => 'group',\n                            'operator' => 'all',\n                            'children' => [\n                                [\n                                    'type' => 'condition',\n                                    'condition' => TrueCondition::class,\n                                    'operator' => '=',\n                                ],\n                                [\n                                    'type' => 'condition',\n                                    'condition' => TrueCondition::class,\n                                    'operator' => '=',\n                                ],\n                            ],\n                        ],\n                        [\n                            'type' => 'group',\n                            'operator' => 'all',\n                            'children' => [\n                                [\n                                    'type' => 'condition',\n                                    'condition' => TrueCondition::class,\n                                    'operator' => '=',\n                                ],\n                                [\n                                    'type' => 'group',\n                                    'operator' => 'any',\n                                    'children' => [\n                                        [\n                                            'type' => 'condition',\n                                            'condition' => FalseCondition::class,\n                                            'operator' => '=',\n                                        ],\n                                        [\n                                            'type' => 'condition',\n                                            'condition' => TrueCondition::class,\n                                            'operator' => '=',\n                                        ],\n                                    ],\n                                ],\n                            ],\n                        ],\n                    ],\n                ],\n                'operator' => 'all',\n                'expected' => true,\n            ],\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/notifications/tests/Fakes/Conditions/FalseCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Tests\\Fakes\\Conditions;\n\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass FalseCondition extends Condition\n{\n    public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool\n    {\n        return false;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/tests/Fakes/Conditions/TrueCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Tests\\Fakes\\Conditions;\n\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass TrueCondition extends Condition\n{\n    public function applies(Notification $notification, ?string $operand, ?string $operator, mixed $value, ?array $meta): bool\n    {\n        return true;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/tests/Fakes/FakeChannel.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Tests\\Fakes;\n\nuse Vigilant\\Notifications\\Channels\\NotificationChannel;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass FakeChannel extends NotificationChannel\n{\n    public static string $name = 'Fake Channel';\n\n    public function fire(Notification $notification, Channel $channel): void\n    {\n        //\n    }\n}\n"
  },
  {
    "path": "packages/notifications/tests/Fakes/FakeNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Tests\\Fakes;\n\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\n\nclass FakeNotification extends Notification\n{\n    public static string $name = 'Fake Notification';\n\n    public string $title = 'Title of this fake notification';\n\n    public string $description = 'Description of this fake notification';\n\n    public Level $level = Level::Critical;\n\n    public function __construct(\n        protected int $number\n    ) {}\n\n    public function uniqueId(): string\n    {\n        return (string) $this->number;\n    }\n}\n"
  },
  {
    "path": "packages/notifications/tests/Models/ChannelTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Tests\\Models;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Notifications\\Channels\\SlackChannel;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Tests\\TestCase;\n\nclass ChannelTest extends TestCase\n{\n    #[Test]\n    public function it_returns_the_internal_name_when_available(): void\n    {\n        $channel = Channel::withoutEvents(function () {\n            return Channel::query()->withoutGlobalScopes()->create([\n                'team_id' => 1,\n                'channel' => SlackChannel::class,\n                'name' => 'Primary Slack',\n                'settings' => [],\n            ]);\n        });\n\n        $this->assertSame('Primary Slack', $channel->title());\n    }\n\n    #[Test]\n    public function it_falls_back_to_the_channel_display_name(): void\n    {\n        $channel = Channel::withoutEvents(function () {\n            return Channel::query()->withoutGlobalScopes()->create([\n                'team_id' => 1,\n                'channel' => SlackChannel::class,\n                'settings' => [],\n            ]);\n        });\n\n        $this->assertSame(SlackChannel::$name, $channel->title());\n    }\n}\n"
  },
  {
    "path": "packages/notifications/tests/Notifications/NotificationRegistryTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Tests\\Notifications;\n\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Notifications\\Tests\\Fakes\\FakeChannel;\nuse Vigilant\\Notifications\\Tests\\Fakes\\FakeNotification;\nuse Vigilant\\Notifications\\Tests\\TestCase;\n\nclass NotificationRegistryTest extends TestCase\n{\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        NotificationRegistry::fake();\n    }\n\n    #[Test]\n    public function it_can_register_notification(): void\n    {\n        NotificationRegistry::registerNotification(FakeNotification::class);\n\n        $this->assertEquals([\n            FakeNotification::class,\n        ], NotificationRegistry::notifications());\n    }\n\n    #[Test]\n    public function it_can_register_channels(): void\n    {\n        NotificationRegistry::registerChannel(FakeChannel::class);\n\n        $this->assertEquals([\n            FakeChannel::class,\n        ], NotificationRegistry::channels());\n    }\n}\n"
  },
  {
    "path": "packages/notifications/tests/Notifications/NotificationTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Tests\\Notifications;\n\nuse Illuminate\\Support\\Facades\\Bus;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Jobs\\SendNotificationJob;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\Notifications\\Models\\Trigger;\nuse Vigilant\\Notifications\\Tests\\Fakes\\FakeChannel;\nuse Vigilant\\Notifications\\Tests\\Fakes\\FakeNotification;\nuse Vigilant\\Notifications\\Tests\\TestCase;\n\nclass NotificationTest extends TestCase\n{\n    #[Test]\n    public function it_dispatches_notification_all_channels_job(): void\n    {\n        Bus::fake();\n        $team = TeamService::fake();\n\n        Channel::query()->create([\n            'team_id' => $team->id,\n            'channel' => FakeChannel::class,\n            'settings' => [],\n        ]);\n\n        Channel::query()->create([\n            'team_id' => $team->id,\n            'channel' => FakeChannel::class,\n            'settings' => [],\n        ]);\n\n        Channel::query()->create([\n            'team_id' => $team->id + 1,\n            'channel' => FakeChannel::class,\n            'settings' => [],\n        ]);\n\n        Trigger::query()->create([\n            'team_id' => $team->id,\n            'notification' => FakeNotification::class,\n            'name' => 'Fake Notification',\n            'conditions' => [],\n            'all_channels' => true,\n        ]);\n\n        Trigger::query()->create([\n            'team_id' => $team->id,\n            'notification' => FakeNotification::class,\n            'name' => 'Fake Notification',\n            'conditions' => [],\n            'all_channels' => true,\n        ]);\n\n        FakeNotification::notify(1);\n\n        Bus::assertDispatchedTimes(SendNotificationJob::class, 4);\n    }\n\n    #[Test]\n    public function it_dispatches_notification_single_channels_job(): void\n    {\n        Bus::fake();\n        $team = TeamService::fake();\n\n        Channel::query()->create([\n            'team_id' => $team->id,\n            'channel' => FakeChannel::class,\n            'settings' => ['a'],\n        ]);\n\n        Channel::query()->create([\n            'team_id' => $team->id,\n            'channel' => FakeChannel::class,\n            'settings' => ['b'],\n        ]);\n\n        /** @var Trigger $trigger */\n        $trigger = Trigger::query()->create([\n            'team_id' => $team->id,\n            'notification' => FakeNotification::class,\n            'name' => 'Fake Notification',\n            'conditions' => [],\n            'all_channels' => false,\n        ]);\n\n        $trigger->channels()->sync([Channel::query()->first()->id ?? 1]);\n\n        FakeNotification::notify(1);\n\n        Bus::assertDispatched(SendNotificationJob::class, function (SendNotificationJob $job): bool {\n            return $job->notification instanceof FakeNotification;\n        });\n    }\n\n    #[Test]\n    public function it_it_arrayable(): void\n    {\n        $notification = FakeNotification::make(1);\n\n        $this->assertEquals([\n            'title' => 'Title of this fake notification',\n            'description' => 'Description of this fake notification',\n            'level' => Level::Critical,\n        ], $notification->toArray());\n    }\n}\n"
  },
  {
    "path": "packages/notifications/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\Notifications\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Livewire\\LivewireServiceProvider;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Notifications\\ServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n            \\Vigilant\\Users\\ServiceProvider::class,\n            \\Vigilant\\Core\\ServiceProvider::class,\n            LivewireServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n\n    }\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        TeamService::fake();\n    }\n}\n"
  },
  {
    "path": "packages/onboarding/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/onboarding/composer.json",
    "content": "{\n    \"name\": \"vigilant/onboarding\",\n    \"description\": \"Vigilant Onboarding\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"guzzlehttp/guzzle\": \"^7.8\",\n        \"laravel/framework\": \"^12.0\",\n        \"livewire/livewire\": \"^3.4\",\n        \"vigilant/core\": \"@dev\",\n        \"vigilant/sites\": \"@dev\",\n        \"vigilant/users\": \"@dev\",\n        \"vigilant/frontend\": \"@dev\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\OnBoarding\\\\\": \"src\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\OnBoarding\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\OnBoarding\\\\ServiceProvider\"\n            ]\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/onboarding/config/onboarding.php",
    "content": "<?php\n\nreturn [\n\n];\n"
  },
  {
    "path": "packages/onboarding/database/migrations/2024_09_29_210000_create_team_onboarding_step_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('team_onboarding_step', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(Team::class)->index();\n\n            $table->string('step')->nullable();\n            $table->dateTime('finished_at')->nullable();\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('team_onboarding_step');\n    }\n};\n"
  },
  {
    "path": "packages/onboarding/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n"
  },
  {
    "path": "packages/onboarding/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/onboarding/resources/views/complete.blade.php",
    "content": "<div wire:init=\"checkStepFinished\" class=\"min-h-screen\">\n    <x-slot name=\"header\">\n        <x-page-header title=\"Get Started with Vigilant\" />\n    </x-slot>\n\n    <!-- Progress Steps -->\n    <div class=\"max-w-4xl mx-auto mb-12\">\n        <div class=\"flex items-center justify-center gap-4\">\n            <!-- Step 1 -->\n            <div class=\"flex items-center\">\n                <div class=\"flex items-center justify-center w-10 h-10 rounded-full bg-green text-base-100 font-bold\">\n                    <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"3\" d=\"M5 13l4 4L19 7\"></path>\n                    </svg>\n                </div>\n                <span class=\"ml-3 text-base-100 font-semibold\">Add Sites</span>\n            </div>\n\n            <!-- Connector -->\n            <div class=\"flex-1 h-1 bg-green mx-4\"></div>\n\n            <!-- Step 2 -->\n            <div class=\"flex items-center\">\n                <div class=\"flex items-center justify-center w-10 h-10 rounded-full bg-green text-base-100 font-bold\">\n                    <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"3\" d=\"M5 13l4 4L19 7\"></path>\n                    </svg>\n                </div>\n                <span class=\"ml-3 text-base-100 font-semibold\">Notifications</span>\n            </div>\n\n            <!-- Connector -->\n            <div class=\"flex-1 h-1 bg-gradient-to-r from-green to-orange mx-4\"></div>\n\n            <!-- Step 3 -->\n            <div class=\"flex items-center\">\n                <div\n                    class=\"flex items-center justify-center w-10 h-10 rounded-full bg-gradient-to-r from-red to-orange text-base-100 font-bold\">\n                    3\n                </div>\n                <span class=\"ml-3 text-base-100 font-semibold\">Done</span>\n            </div>\n        </div>\n    </div>\n\n    <div class=\"max-w-4xl mx-auto mb-8 text-center\">\n        <div class=\"mb-6\">\n            <div class=\"w-20 h-20 mx-auto mb-4 rounded-full bg-green/20 flex items-center justify-center\">\n                <svg class=\"w-10 h-10 text-green\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\"></path>\n                </svg>\n            </div>\n        </div>\n\n        <h2 class=\"text-3xl font-bold mb-4\">\n            <span\n                class=\"bg-gradient-to-r from-base-50 via-base-100 to-base-200 bg-clip-text text-transparent\">@lang('You\\'re all set!')</span>\n            <span class=\"inline-block\">🎉</span>\n        </h2>\n        <p class=\"text-lg text-base-300 max-w-2xl mx-auto\">\n            @lang('Vigilant is now monitoring your websites. Here\\'s what happens next.')\n        </p>\n    </div>\n\n    <div class=\"max-w-4xl mx-auto mb-12\">\n        <div class=\"space-y-4\">\n            <a href=\"{{ route('sites') }}\" wire:navigate.hover\n                class=\"block bg-base-850 border border-base-700 rounded-lg p-6 transition-all duration-200 hover:border-blue/50 hover:shadow-lg hover:shadow-blue/10 hover:-translate-y-0.5 cursor-pointer group\">\n                <div class=\"flex items-start gap-4\">\n                    <div\n                        class=\"flex-shrink-0 w-10 h-10 rounded-lg bg-blue/20 flex items-center justify-center group-hover:bg-blue/30 transition-colors duration-200\">\n                        <svg class=\"w-5 h-5 text-blue\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n                                d=\"M13 10V3L4 14h7v7l9-11h-7z\"></path>\n                        </svg>\n                    </div>\n                    <div class=\"flex-1\">\n                        <h3\n                            class=\"text-base-100 font-semibold mb-1 group-hover:text-blue transition-colors duration-200\">\n                            @lang('Initial Checks Running')</h3>\n                        <p class=\"text-base-400 text-sm\">\n                            @lang('We\\'re running the first checks on your websites right now. This usually takes a few hours. You\\'ll see the results appear in your dashboard soon.')\n                        </p>\n                    </div>\n                    <svg class=\"w-5 h-5 text-base-600 group-hover:text-blue transition-all duration-200 group-hover:translate-x-1\"\n                        fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n                    </svg>\n                </div>\n            </a>\n\n            <a href=\"{{ route('notifications') }}\" wire:navigate.hover\n                class=\"block bg-base-850 border border-base-700 rounded-lg p-6 transition-all duration-200 hover:border-orange/50 hover:shadow-lg hover:shadow-orange/10 hover:-translate-y-0.5 cursor-pointer group\">\n                <div class=\"flex items-start gap-4\">\n                    <div\n                        class=\"flex-shrink-0 w-10 h-10 rounded-lg bg-orange/20 flex items-center justify-center group-hover:bg-orange/30 transition-colors duration-200\">\n                        <svg class=\"w-5 h-5 text-orange\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n                                d=\"M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9\">\n                            </path>\n                        </svg>\n                    </div>\n                    <div class=\"flex-1\">\n                        <h3\n                            class=\"text-base-100 font-semibold mb-1 group-hover:text-orange transition-colors duration-200\">\n                            @lang('Notifications Are Active')</h3>\n                        <p class=\"text-base-400 text-sm\">\n                            @lang('You\\'ll receive alerts when we detect issues with your websites. Make sure to check your notification channels are working correctly.')\n                        </p>\n                    </div>\n                    <svg class=\"w-5 h-5 text-base-600 group-hover:text-orange transition-all duration-200 group-hover:translate-x-1\"\n                        fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n                    </svg>\n                </div>\n            </a>\n\n            <a href=\"{{ route('cve.index') }}\" wire:navigate.hover\n                class=\"block bg-base-850 border border-base-700 rounded-lg p-6 transition-all duration-200 hover:border-red/50 hover:shadow-lg hover:shadow-red/10 hover:-translate-y-0.5 cursor-pointer group\">\n                <div class=\"flex items-start gap-4\">\n                    <div\n                        class=\"flex-shrink-0 w-10 h-10 rounded-lg bg-red/20 flex items-center justify-center group-hover:bg-red/30 transition-colors duration-200\">\n                        <svg class=\"w-5 h-5 text-red\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n                                d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\">\n                            </path>\n                        </svg>\n                    </div>\n                    <div class=\"flex-1\">\n                        <h3\n                            class=\"text-base-100 font-semibold mb-1 group-hover:text-red transition-colors duration-200\">\n                            @lang('Setup CVE Monitoring')</h3>\n                        <p class=\"text-base-400 text-sm\">\n                            @lang('Get alerted about security vulnerabilities affecting your technology stack. Configure CVE monitors to track packages and technologies you use.')\n                        </p>\n                    </div>\n                    <svg class=\"w-5 h-5 text-base-600 group-hover:text-red transition-all duration-200 group-hover:translate-x-1\"\n                        fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\">\n                        </path>\n                    </svg>\n                </div>\n            </a>\n        </div>\n    </div>\n\n    <div class=\"max-w-4xl mx-auto flex items-center justify-end\">\n        <a type=\"button\" href=\"{{ route('sites') }}\" wire:navigate.hover\n            class=\"px-6 py-3 bg-gradient-to-r from-red to-orange text-base-100 font-semibold rounded-lg hover:shadow-lg hover:shadow-orange/20 transition-all duration-200\">\n            @lang('View My Sites →')\n        </a>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/onboarding/resources/views/import-domains.blade.php",
    "content": "<div wire:init=\"checkStepFinished\" class=\"min-h-screen\">\n    <x-slot name=\"header\">\n        <x-page-header title=\"Get Started with Vigilant\" />\n    </x-slot>\n\n    <div class=\"max-w-4xl mx-auto mb-12\">\n        <div class=\"flex items-center justify-center gap-4\">\n            <!-- Step 1 -->\n            <div class=\"flex items-center\">\n                <div\n                    class=\"flex items-center justify-center w-10 h-10 rounded-full bg-gradient-to-r from-red to-orange text-base-100 font-bold\">\n                    1\n                </div>\n                <span class=\"ml-3 text-base-100 font-semibold\">Add Sites</span>\n            </div>\n\n            <div class=\"flex-1 h-1 bg-base-700 mx-4\"></div>\n\n            <!-- Step 2 -->\n            <div class=\"flex items-center opacity-50\">\n                <div\n                    class=\"flex items-center justify-center w-10 h-10 rounded-full bg-base-700 text-base-400 font-bold\">\n                    2\n                </div>\n                <span class=\"ml-3 text-base-400 font-semibold\">Notifications</span>\n            </div>\n\n            <!-- Connector -->\n            <div class=\"flex-1 h-1 bg-base-700 mx-4\"></div>\n\n            <!-- Step 3 -->\n            <div class=\"flex items-center opacity-50\">\n                <div\n                    class=\"flex items-center justify-center w-10 h-10 rounded-full bg-base-700 text-base-400 font-bold\">\n                    3\n                </div>\n                <span class=\"ml-3 text-base-400 font-semibold\">Done</span>\n            </div>\n        </div>\n    </div>\n\n    <div class=\"max-w-4xl mx-auto mb-8 text-center\">\n        <h2 class=\"text-3xl font-bold mb-4\">\n            <span\n                class=\"bg-gradient-to-r from-base-50 via-base-100 to-base-200 bg-clip-text text-transparent\">@lang('Welcome to Vigilant, :name!', ['name' => $name])</span>\n            <span class=\"inline-block\">👋</span>\n        </h2>\n        <p class=\"text-lg text-base-300 max-w-2xl mx-auto\">\n            @lang('Let\\'s get you started by importing your websites. Add your domains below and we\\'ll set up monitoring for you.')\n        </p>\n    </div>\n\n    <!-- Import Form -->\n    <div class=\"max-w-4xl mx-auto\">\n        <livewire:sites.import :inline=\"true\" />\n\n        <!-- Skip Option -->\n        <div class=\"mt-6 flex items-center justify-between\">\n            <button wire:click=\"skipOnboarding\"\n                class=\"text-base-400 hover:text-base-200 text-sm transition-colors duration-200\">\n                @lang('Skip onboarding entirely')\n            </button>\n            <button wire:click=\"redirectNextStep\"\n                class=\"text-base-400 hover:text-base-200 text-sm transition-colors duration-200\">\n                @lang('Skip this step →')\n            </button>\n        </div>\n    </div>\n\n    <!-- Features Preview -->\n    <div class=\"max-w-4xl mx-auto mt-16\">\n        <h3 class=\"text-xl font-bold text-base-100 mb-6 text-center\">@lang('What you\\'ll get')</h3>\n        <div class=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n            <div class=\"bg-base-850 border border-base-700 rounded-lg p-6 text-center\">\n                <div class=\"w-12 h-12 mx-auto mb-4 rounded-lg bg-blue/20 flex items-center justify-center\">\n                    <svg class=\"w-6 h-6 text-blue\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n                            d=\"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n                    </svg>\n                </div>\n                <h4 class=\"font-semibold text-base-100 mb-2\">@lang('Uptime Monitoring')</h4>\n                <p class=\"text-sm text-base-400\">@lang('24/7 monitoring to ensure your sites stay online')</p>\n            </div>\n\n            <div class=\"bg-base-850 border border-base-700 rounded-lg p-6 text-center\">\n                <div class=\"w-12 h-12 mx-auto mb-4 rounded-lg bg-green/20 flex items-center justify-center\">\n                    <svg class=\"w-6 h-6 text-green\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n                            d=\"M13 10V3L4 14h7v7l9-11h-7z\"></path>\n                    </svg>\n                </div>\n                <h4 class=\"font-semibold text-base-100 mb-2\">@lang('Performance Tracking')</h4>\n                <p class=\"text-sm text-base-400\">@lang('Monitor site speed and performance metrics')</p>\n            </div>\n\n            <div class=\"bg-base-850 border border-base-700 rounded-lg p-6 text-center\">\n                <div class=\"w-12 h-12 mx-auto mb-4 rounded-lg bg-orange/20 flex items-center justify-center\">\n                    <svg class=\"w-6 h-6 text-orange\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n                            d=\"M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9\">\n                        </path>\n                    </svg>\n                </div>\n                <h4 class=\"font-semibold text-base-100 mb-2\">@lang('Instant Alerts')</h4>\n                <p class=\"text-sm text-base-400\">@lang('Get notified immediately when issues arise')</p>\n            </div>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/onboarding/resources/views/notification-channel.blade.php",
    "content": "<div wire:init=\"checkStepFinished\" class=\"min-h-screen\">\n    <x-slot name=\"header\">\n        <x-page-header title=\"Setup Notifications\" />\n    </x-slot>\n\n    <!-- Progress Steps -->\n    <div class=\"max-w-4xl mx-auto mb-12\">\n        <div class=\"flex items-center justify-center gap-4\">\n            <!-- Step 1 -->\n            <div class=\"flex items-center opacity-75\">\n                <div class=\"flex items-center justify-center w-10 h-10 rounded-full bg-green text-base-100 font-bold\">\n                    <svg class=\"w-6 h-6\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\"></path>\n                    </svg>\n                </div>\n                <span class=\"ml-3 text-base-300 font-semibold\">Add Sites</span>\n            </div>\n            \n            <!-- Connector -->\n            <div class=\"flex-1 h-1 bg-green mx-4\"></div>\n            \n            <!-- Step 2 -->\n            <div class=\"flex items-center\">\n                <div class=\"flex items-center justify-center w-10 h-10 rounded-full bg-gradient-to-r from-red to-orange text-base-100 font-bold\">\n                    2\n                </div>\n                <span class=\"ml-3 text-base-100 font-semibold\">Notifications</span>\n            </div>\n            \n            <!-- Connector -->\n            <div class=\"flex-1 h-1 bg-base-700 mx-4\"></div>\n            \n            <!-- Step 3 -->\n            <div class=\"flex items-center opacity-50\">\n                <div class=\"flex items-center justify-center w-10 h-10 rounded-full bg-base-700 text-base-400 font-bold\">\n                    3\n                </div>\n                <span class=\"ml-3 text-base-400 font-semibold\">Done</span>\n            </div>\n        </div>\n    </div>\n\n    <!-- Welcome Message -->\n    <div class=\"max-w-4xl mx-auto mb-8 text-center\">\n        <h2 class=\"text-3xl font-bold bg-gradient-to-r from-base-50 via-base-100 to-base-200 bg-clip-text text-transparent mb-4\">\n            @lang('Setup Your Notification Channel')\n        </h2>\n        <p class=\"text-lg text-base-300 max-w-2xl mx-auto\">\n            @lang('Choose how you want to receive alerts when your sites have issues. You can add more channels later.')\n        </p>\n    </div>\n\n    <!-- Channel Form -->\n    <div class=\"max-w-4xl mx-auto\">\n        <livewire:channel-form :inline=\"true\" :channel=\"$channel\" />\n        \n        <!-- Navigation -->\n        <div class=\"mt-6 flex items-center justify-between\">\n            <div class=\"flex items-center gap-4\">\n                <button wire:click=\"goBack\" class=\"inline-flex items-center gap-2 text-base-400 hover:text-base-200 text-sm transition-colors duration-200\">\n                    <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\"></path>\n                    </svg>\n                    @lang('Back to add sites')\n                </button>\n                <button wire:click=\"skipOnboarding\" class=\"text-base-400 hover:text-base-200 text-sm transition-colors duration-200\">\n                    @lang('Skip onboarding entirely')\n                </button>\n            </div>\n            <button wire:click=\"redirectNextStep\" class=\"text-base-400 hover:text-base-200 text-sm transition-colors duration-200\">\n                @lang('Skip this step →')\n            </button>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/onboarding/routes/web.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\OnBoarding\\Http\\Middleware\\OnlyOnboarding;\nuse Vigilant\\OnBoarding\\Livewire\\Complete;\nuse Vigilant\\OnBoarding\\Livewire\\ImportDomains;\nuse Vigilant\\OnBoarding\\Livewire\\NotificationChannel;\n\nRoute::middleware(OnlyOnboarding::class)->group(function () {\n    Route::get('setup', ImportDomains::class)\n        ->name('onboard');\n\n    Route::get('setup/notifications', NotificationChannel::class)\n        ->name('onboard.notifications');\n\n    Route::get('setup/complete', Complete::class)\n        ->name('onboard.complete');\n});\n"
  },
  {
    "path": "packages/onboarding/src/Actions/ShouldOnboard.php",
    "content": "<?php\n\nnamespace Vigilant\\OnBoarding\\Actions;\n\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\OnBoarding\\Models\\OnboardingStep;\n\nclass ShouldOnboard\n{\n    public function __construct(protected TeamService $teamService) {}\n\n    public function shouldOnboard(): bool\n    {\n        /** @var TeamService $teamService */\n        $teamService = app(TeamService::class);\n\n        $team = $teamService->team();\n\n        /** @var ?OnboardingStep $onBoard */\n        $onBoard = OnboardingStep::query()\n            ->where('team_id', '=', $team->id)\n            ->where('step', '=', 'complete')\n            ->first();\n\n        if ($onBoard !== null && $onBoard->finished_at !== null) {\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "packages/onboarding/src/Http/Middleware/OnlyOnboarding.php",
    "content": "<?php\n\nnamespace Vigilant\\OnBoarding\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Http\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Vigilant\\OnBoarding\\Actions\\ShouldOnboard;\n\nclass OnlyOnboarding\n{\n    /**\n     * @param  \\Closure(\\Illuminate\\Http\\Request): (\\Symfony\\Component\\HttpFoundation\\Response)  $next\n     */\n    public function handle(Request $request, Closure $next): Response\n    {\n        /** @var ShouldOnboard $shouldOnboard */\n        $shouldOnboard = app(ShouldOnboard::class);\n\n        if (! $shouldOnboard->shouldOnboard()) {\n            return redirect()->route('sites');\n        }\n\n        return $next($request);\n\n    }\n}\n"
  },
  {
    "path": "packages/onboarding/src/Http/Middleware/RedirectToOnboard.php",
    "content": "<?php\n\nnamespace Vigilant\\OnBoarding\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Route;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Vigilant\\OnBoarding\\Actions\\ShouldOnboard;\nuse Vigilant\\Users\\Models\\User;\n\nclass RedirectToOnboard\n{\n    /**\n     * @param  \\Closure(\\Illuminate\\Http\\Request): (\\Symfony\\Component\\HttpFoundation\\Response)  $next\n     */\n    public function handle(Request $request, Closure $next): Response\n    {\n        /** @var ShouldOnboard $shouldOnboard */\n        $shouldOnboard = app(ShouldOnboard::class);\n\n        /** @var ?User $user */\n        $user = auth()->user();\n\n        if (\n            $user === null ||\n            $user->email_verified_at === null ||\n            Route::is('onboard*') ||\n            Route::is('livewire.*') ||\n            Route::is('default.livewire.*') ||\n            Route::is('quick-setup') ||\n            ! $shouldOnboard->shouldOnboard()\n        ) {\n            return $next($request);\n        }\n\n        return redirect()->route('onboard');\n    }\n}\n"
  },
  {
    "path": "packages/onboarding/src/Livewire/Complete.php",
    "content": "<?php\n\nnamespace Vigilant\\OnBoarding\\Livewire;\n\nuse Livewire\\Component;\nuse Vigilant\\OnBoarding\\Models\\OnboardingStep;\nuse Vigilant\\Users\\Models\\User;\n\nclass Complete extends Component\n{\n    public function finish(): void\n    {\n        /** @var User $user */\n        $user = auth()->user();\n\n        OnboardingStep::query()->updateOrCreate(\n            ['team_id' => $user->currentTeam?->id],\n            ['step' => 'complete', 'finished_at' => now()]\n        );\n\n        $this->redirectRoute('sites');\n    }\n\n    public function goBack(): void\n    {\n        $this->redirectRoute('onboard.notifications');\n    }\n\n    public function checkStepFinished(): void\n    {\n        /** @var User $user */\n        $user = auth()->user();\n\n        OnboardingStep::query()->updateOrCreate(\n            ['team_id' => $user->currentTeam?->id],\n            ['step' => 'complete', 'finished_at' => now()]\n        );\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'onboarding::complete';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/onboarding/src/Livewire/ImportDomains.php",
    "content": "<?php\n\nnamespace Vigilant\\OnBoarding\\Livewire;\n\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\OnBoarding\\Models\\OnboardingStep;\nuse Vigilant\\Users\\Models\\User;\n\nclass ImportDomains extends Component\n{\n    #[On('sites-imported')]\n    public function redirectNextStep(): void\n    {\n        /** @var User $user */\n        $user = auth()->user();\n\n        OnboardingStep::query()->updateOrCreate(\n            ['team_id' => $user->currentTeam?->id],\n            ['step' => 'domain-import', 'finished_at' => now()]\n        );\n\n        $this->redirectRoute('onboard.notifications');\n    }\n\n    public function checkStepFinished(): void\n    {\n        // Allow users to return to this step even if completed\n    }\n\n    public function skipOnboarding(): void\n    {\n        /** @var User $user */\n        $user = auth()->user();\n\n        OnboardingStep::query()->updateOrCreate(\n            ['team_id' => $user->currentTeam?->id],\n            ['step' => 'complete', 'finished_at' => now()]\n        );\n\n        $this->redirectRoute('sites');\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'onboarding::import-domains';\n\n        return view($view, [\n            'name' => ucfirst(auth()->user()->name ?? 'User'),\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/onboarding/src/Livewire/NotificationChannel.php",
    "content": "<?php\n\nnamespace Vigilant\\OnBoarding\\Livewire;\n\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\Notifications\\Channels\\MailChannel;\nuse Vigilant\\Notifications\\Models\\Channel;\nuse Vigilant\\OnBoarding\\Models\\OnboardingStep;\nuse Vigilant\\Users\\Models\\User;\n\nclass NotificationChannel extends Component\n{\n    public Channel $channel;\n\n    public function mount(): void\n    {\n        /** @var User $user */\n        $user = auth()->user();\n\n        $this->channel = new Channel([\n            'channel' => MailChannel::class,\n            'name' => 'E-mail',\n            'settings' => [\n                'to' => $user->email,\n            ],\n        ]);\n    }\n\n    #[On('channel-saved')]\n    public function redirectNextStep(): void\n    {\n        /** @var User $user */\n        $user = auth()->user();\n\n        OnboardingStep::query()->updateOrCreate(\n            ['team_id' => $user->currentTeam?->id],\n            ['step' => 'notification-channel', 'finished_at' => now()]\n        );\n\n        $this->redirectRoute('onboard.complete');\n    }\n\n    public function goBack(): void\n    {\n        /** @var User $user */\n        $user = auth()->user();\n\n        // Clear the current step so users can go back\n        OnboardingStep::query()\n            ->where('team_id', '=', $user->currentTeam?->id)\n            ->where('step', '=', 'notification-channel')\n            ->delete();\n\n        $this->redirectRoute('onboard');\n    }\n\n    public function checkStepFinished(): void\n    {\n        /** @var User $user */\n        $user = auth()->user();\n\n        $onBoard = OnboardingStep::query()\n            ->where('team_id', '=', $user->currentTeam?->id)\n            ->where('step', '=', 'notification-channel')\n            ->first();\n\n        if ($onBoard !== null && $onBoard->finished_at !== null) {\n            $this->redirectRoute('onboard.complete');\n        }\n    }\n\n    public function skipOnboarding(): void\n    {\n        /** @var User $user */\n        $user = auth()->user();\n\n        OnboardingStep::query()->updateOrCreate(\n            ['team_id' => $user->currentTeam?->id],\n            ['step' => 'complete', 'finished_at' => now()]\n        );\n\n        $this->redirectRoute('sites');\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'onboarding::notification-channel';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/onboarding/src/Models/OnboardingStep.php",
    "content": "<?php\n\nnamespace Vigilant\\OnBoarding\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Carbon;\n\n/**\n * @property int $id\n * @property int $team_id\n * @property ?string $step\n * @property ?Carbon $finished_at\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n */\nclass OnboardingStep extends Model\n{\n    protected $table = 'team_onboarding_step';\n\n    protected $guarded = [];\n}\n"
  },
  {
    "path": "packages/onboarding/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\OnBoarding;\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\nuse Livewire\\Livewire;\nuse Vigilant\\OnBoarding\\Livewire\\Complete;\nuse Vigilant\\OnBoarding\\Livewire\\ImportDomains;\nuse Vigilant\\OnBoarding\\Livewire\\NotificationChannel;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function register(): void\n    {\n        $this\n            ->registerConfig();\n    }\n\n    protected function registerConfig(): static\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/onboarding.php', 'onboarding');\n\n        return $this;\n    }\n\n    public function boot(): void\n    {\n        $this\n            ->bootConfig()\n            ->bootMigrations()\n            ->bootCommands()\n            ->bootViews()\n            ->bootLivewire()\n            ->bootRoutes();\n    }\n\n    protected function bootConfig(): static\n    {\n        $this->publishes([\n            __DIR__.'/../config/onboarding.php' => config_path('onboarding.php'),\n        ], 'config');\n\n        return $this;\n    }\n\n    protected function bootMigrations(): static\n    {\n        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');\n\n        return $this;\n    }\n\n    protected function bootCommands(): static\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n\n            ]);\n        }\n\n        return $this;\n    }\n\n    protected function bootViews(): static\n    {\n        $this->loadViewsFrom(__DIR__.'/../resources/views', 'onboarding');\n\n        return $this;\n    }\n\n    protected function bootLivewire(): static\n    {\n        Livewire::component('onboarding-import-domains', ImportDomains::class);\n        Livewire::component('onboarding-monitoring-channel', NotificationChannel::class);\n        Livewire::component('onboarding-complete', Complete::class);\n\n        return $this;\n    }\n\n    protected function bootRoutes(): static\n    {\n        if (! $this->app->routesAreCached()) {\n            Route::middleware(['web', 'auth'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/web.php'));\n        }\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/onboarding/testbench.yaml",
    "content": "providers:\n  - Vigilant\\OnBoarding\\ServiceProvider\n"
  },
  {
    "path": "packages/onboarding/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\OnBoarding\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Livewire\\LivewireServiceProvider;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\OnBoarding\\ServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        TeamService::fake();\n    }\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n            \\Vigilant\\Users\\ServiceProvider::class,\n            LivewireServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/settings/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/settings/composer.json",
    "content": "{\n    \"name\": \"vigilant/settings\",\n    \"description\": \"Vigilant settings\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"guzzlehttp/guzzle\": \"^7.8\",\n        \"laravel/framework\": \"^12.0\",\n        \"livewire/livewire\": \"^3.4\",\n        \"vigilant/core\": \"@dev\",\n        \"vigilant/sites\": \"@dev\",\n        \"vigilant/users\": \"@dev\",\n        \"vigilant/frontend\": \"@dev\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Settings\\\\\": \"src\",\n            \"Vigilant\\\\Settings\\\\Database\\\\Factories\\\\\": \"database/factories\",\n            \"Vigilant\\\\Users\\\\Database\\\\Factories\\\\\": \"../users/database/factories\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Settings\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\Settings\\\\ServiceProvider\"\n            ]\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/settings/config/settings.php",
    "content": "<?php\n\nreturn [\n\n];\n"
  },
  {
    "path": "packages/settings/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n    ignoreErrors:\n        - identifier: missingType.iterableValue\n"
  },
  {
    "path": "packages/settings/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/settings/resources/views/index.blade.php",
    "content": "<x-slot name=\"header\">\n    <x-page-header title=\"Settings\">\n    </x-page-header>\n</x-slot>\n\n<div x-data=\"{ activeTab: @entangle('tab') }\" class=\"pb-10\">\n    \n    {{-- Tab Navigation --}}\n    <div class=\"max-w-7xl mx-auto mb-8\">\n        <x-frontend::card :padding=\"false\" class=\"overflow-hidden\">\n            <div class=\"border-b border-base-700\">\n                <nav class=\"-mb-px flex space-x-1 p-2\" aria-label=\"Tabs\">\n                    @foreach($tabs as $key => $data)\n                        <button\n                            type=\"button\"\n                            @click=\"activeTab = '{{ $key }}'\"\n                            :class=\"activeTab === '{{ $key }}' ? 'border-red text-base-50 bg-base-800/50' : 'border-transparent text-base-300 hover:text-base-100 hover:border-base-600'\"\n                            class=\"group relative min-w-0 flex-1 sm:flex-initial overflow-hidden rounded-lg border-2 px-4 py-3 text-center text-sm font-medium transition-all duration-300 focus:z-10 focus:outline-none focus:ring-2 focus:ring-red focus:ring-offset-2 focus:ring-offset-base-900 @if($loop->first) border-red text-base-50 bg-base-800/50 @else border-transparent text-base-300 @endif\"\n                        >\n                            <span class=\"flex items-center justify-center gap-2\">\n                                <span>{{ $data['title'] }}</span>\n                            </span>\n                        </button>\n                    @endforeach\n                </nav>\n            </div>\n        </x-frontend::card>\n    </div>\n\n    {{-- Tab Panels --}}\n    <div class=\"space-y-6\">\n        @foreach($tabs as $key => $data)\n            <div x-show=\"activeTab === '{{ $key }}'\" \n                 @if(!$loop->first)x-cloak @endif\n                 x-transition:enter=\"transition ease-out duration-300\"\n                 x-transition:enter-start=\"opacity-0 transform translate-y-4\"\n                 x-transition:enter-end=\"opacity-100 transform translate-y-0\">\n                @if(array_key_exists('component', $data))\n                    <livewire:dynamic-component :is=\"$data['component']\" wire:key=\"{{ $key }}\" />\n                @endif\n            </div>\n        @endforeach\n    </div>\n\n</div>\n\n"
  },
  {
    "path": "packages/settings/resources/views/tabs/profile.blade.php",
    "content": "<div class=\"max-w-7xl mx-auto\">\n\n    <form wire:submit=\"save\">\n        <div class=\"flex flex-col gap-4 max-w-7xl mx-auto\">\n\n            <x-form.header>@lang('Profile')</x-form.header>\n\n            <x-form.text class=\"sm:col-span-2\"\n                         field=\"form.name\"\n                         name=\"Name\"\n                         required\n                         autocomplete=\"name\"\n            />\n\n            <x-form.text class=\"sm:col-span-2\"\n                         field=\"form.email\"\n                         name=\"E-Mail\"\n                         description=\"Your account's E-mail address\"\n                         required\n                         autocomplete=\"email\"\n            />\n\n            <x-form.submit-button submitText=\"Save\"/>\n        </div>\n    </form>\n\n\n    {{--    Password change--}}\n\n    {{--     Delete account --}}\n\n\n{{--        @if (Laravel\\Fortify\\Features::enabled(Laravel\\Fortify\\Features::updatePasswords()))--}}\n{{--            <div class=\"mt-10 sm:mt-0\">--}}\n{{--                @livewire('profile.update-password-form')--}}\n{{--            </div>--}}\n\n{{--            <x-section-border/>--}}\n{{--        @endif--}}\n\n    {{--    @if (Laravel\\Jetstream\\Jetstream::hasAccountDeletionFeatures())--}}\n    {{--        <x-section-border/>--}}\n\n    {{--        <div class=\"mt-10 sm:mt-0\">--}}\n    {{--            @livewire('profile.delete-user-form')--}}\n    {{--        </div>--}}\n    {{--    @endif--}}\n</div>\n"
  },
  {
    "path": "packages/settings/resources/views/tabs/security.blade.php",
    "content": "<div class=\"max-w-7xl mx-auto\">\n    <form wire:submit=\"updatePassword\">\n        <div class=\"flex flex-col gap-4 max-w-7xl mx-auto\">\n\n            <x-form.header>@lang('Password')</x-form.header>\n\n            <x-form.password class=\"sm:col-span-2\"\n                             field=\"password.current_password\"\n                             name=\"Current Password\"\n                             live=\"false\"\n            />\n\n            <x-form.password class=\"sm:col-span-2\"\n                             field=\"password.password\"\n                             name=\"New Password\"\n                             live=\"false\"\n            />\n\n            <x-form.password class=\"sm:col-span-2\"\n                             field=\"password.password_confirmation\"\n                             name=\"New Password Confirmation\"\n                             live=\"false\"\n            />\n\n            <x-form.submit-button submitText=\"Change\"/>\n        </div>\n    </form>\n\n    <x-form.header>@lang('Additional Security Settings')</x-form.header>\n\n    @if (Laravel\\Fortify\\Features::canManageTwoFactorAuthentication())\n        <div class=\"mt-6\">\n            @livewire('profile.two-factor-authentication-form')\n        </div>\n    @endif\n\n    <div class=\"mt-6\">\n        @livewire('profile.logout-other-browser-sessions-form')\n    </div>\n\n</div>\n"
  },
  {
    "path": "packages/settings/resources/views/tabs/team.blade.php",
    "content": "<div class=\"max-w-7xl mx-auto pt-6\">\n    @livewire('teams.update-team-name-form', ['team' => $team])\n\n    @livewire('teams.team-member-manager', ['team' => $team])\n\n    @if (Gate::check('delete', $team) && ! $team->personal_team)\n        <x-section-border/>\n\n        <div class=\"mt-10 sm:mt-0\">\n            @livewire('teams.delete-team-form', ['team' => $team])\n        </div>\n    @endif\n</div>\n"
  },
  {
    "path": "packages/settings/routes/web.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\Settings\\Livewire\\Settings;\n\nRoute::get('settings', Settings::class)->name('settings');\n"
  },
  {
    "path": "packages/settings/src/Livewire/Forms/ProfileForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Settings\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Form;\n\nclass ProfileForm extends Form\n{\n    #[Validate('required|max:255')]\n    public string $name = '';\n\n    #[Validate('required|email|max:255')]\n    public string $email = '';\n}\n"
  },
  {
    "path": "packages/settings/src/Livewire/Forms/UpdatePasswordForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Settings\\Livewire\\Forms;\n\nuse Livewire\\Form;\nuse Vigilant\\Users\\Actions\\Fortify\\PasswordValidationRules;\n\nclass UpdatePasswordForm extends Form\n{\n    use PasswordValidationRules;\n\n    public string $current_password = '';\n\n    public string $password = '';\n\n    public string $password_confirmation = '';\n\n    public function rules(): array\n    {\n        return [\n            'current_password' => ['required', 'string', 'current_password:web'],\n            'password' => $this->passwordRules(),\n        ];\n    }\n\n    public function messages(): array\n    {\n        return [\n            'current_password.current_password' => __('The provided password does not match your current password.'),\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/settings/src/Livewire/Settings.php",
    "content": "<?php\n\nnamespace Vigilant\\Settings\\Livewire;\n\nuse Illuminate\\Support\\Arr;\nuse Laravel\\Jetstream\\Jetstream;\nuse Livewire\\Attributes\\Url;\nuse Livewire\\Component;\n\nclass Settings extends Component\n{\n    #[Url]\n    public string $tab = '';\n\n    public function mount(): void\n    {\n        if (blank($this->tab)) {\n            /** @var string $tab */\n            $tab = Arr::first(array_keys($this->tabs()));\n            $this->tab = $tab;\n        }\n    }\n\n    protected function tabs(): array\n    {\n        $tabs = [];\n\n        $tabs['profile'] = [\n            'title' => 'Profile',\n            'component' => 'settings-tab-profile',\n        ];\n\n        $tabs['security'] = [\n            'title' => 'Account Security',\n            'component' => 'settings-tab-security',\n        ];\n\n        if (Jetstream::hasTeamFeatures()) {\n            $tabs['team'] = [\n                'title' => 'Team Settings',\n                'component' => 'settings-tab-team',\n            ];\n        }\n\n        if (! ce()) {\n            $tabs['billing'] = [\n                'title' => 'Billing',\n                'component' => 'settings-tab-billing',\n            ];\n\n            $tabs['company'] = [\n                'title' => 'Company Info',\n                'component' => 'settings-tab-company-info',\n            ];\n        }\n\n        return $tabs;\n    }\n\n    public function render(): mixed\n    {\n\n        /** @var view-string $view */\n        $view = 'settings::index';\n\n        return view($view, [\n            'tabs' => $this->tabs(),\n            'tab' => $this->tab,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/settings/src/Livewire/Tabs/Profile.php",
    "content": "<?php\n\nnamespace Vigilant\\Settings\\Livewire\\Tabs;\n\nuse Livewire\\Component;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Settings\\Livewire\\Forms\\ProfileForm;\nuse Vigilant\\Users\\Models\\User;\n\nclass Profile extends Component\n{\n    use DisplaysAlerts;\n\n    public ProfileForm $form;\n\n    public function mount(): void\n    {\n        /** @var User $user */\n        $user = auth()->user();\n\n        $this->form->fill($user->toArray());\n    }\n\n    public function save(): void\n    {\n        $this->validate();\n\n        /** @var User $user */\n        $user = auth()->user();\n\n        $validated = $this->form->validate();\n        $user->update($validated);\n\n        $this->alert(\n            __('Saved'),\n            __('Profile information saved'),\n            AlertType::Success\n        );\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'settings::tabs.profile';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/settings/src/Livewire/Tabs/Security.php",
    "content": "<?php\n\nnamespace Vigilant\\Settings\\Livewire\\Tabs;\n\nuse Illuminate\\Support\\Facades\\Hash;\nuse Livewire\\Component;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Settings\\Livewire\\Forms\\UpdatePasswordForm;\nuse Vigilant\\Users\\Models\\User;\n\nclass Security extends Component\n{\n    use DisplaysAlerts;\n\n    public UpdatePasswordForm $password;\n\n    public function updatePassword(): void\n    {\n        $validated = $this->password->validate();\n\n        /** @var User $user */\n        $user = auth()->user();\n\n        $user->forceFill([\n            'password' => Hash::make($validated['password']),\n        ])->save();\n\n        $this->password->reset();\n\n        $this->alertBrowser(\n            __('Saved'),\n            __('Password changed'),\n            AlertType::Success\n        );\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'settings::tabs.security';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/settings/src/Livewire/Tabs/Team.php",
    "content": "<?php\n\nnamespace Vigilant\\Settings\\Livewire\\Tabs;\n\nuse Livewire\\Component;\nuse Vigilant\\Users\\Models\\User;\n\nclass Team extends Component\n{\n    public function render(): mixed\n    {\n        /** @var User $user */\n        $user = auth()->user();\n\n        /** @var view-string $view */\n        $view = 'settings::tabs.team';\n\n        return view($view, [\n            'team' => $user->currentTeam,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/settings/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\Settings;\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\nuse Livewire\\Livewire;\nuse Vigilant\\Settings\\Livewire\\Tabs\\Profile;\nuse Vigilant\\Settings\\Livewire\\Tabs\\Security;\nuse Vigilant\\Settings\\Livewire\\Tabs\\Team;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function register(): void\n    {\n        $this\n            ->registerConfig();\n    }\n\n    protected function registerConfig(): static\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/settings.php', 'settings');\n\n        return $this;\n    }\n\n    public function boot(): void\n    {\n        $this\n            ->bootConfig()\n            ->bootMigrations()\n            ->bootCommands()\n            ->bootViews()\n            ->bootLivewire()\n            ->bootRoutes();\n    }\n\n    protected function bootConfig(): static\n    {\n        $this->publishes([\n            __DIR__.'/../config/settings.php' => config_path('settings.php'),\n        ], 'config');\n\n        return $this;\n    }\n\n    protected function bootMigrations(): static\n    {\n        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');\n\n        return $this;\n    }\n\n    protected function bootCommands(): static\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n\n            ]);\n        }\n\n        return $this;\n    }\n\n    protected function bootViews(): static\n    {\n        $this->loadViewsFrom(__DIR__.'/../resources/views', 'settings');\n\n        return $this;\n    }\n\n    protected function bootLivewire(): static\n    {\n        Livewire::component('settings-tab-profile', Profile::class);\n        Livewire::component('settings-tab-team', Team::class);\n        Livewire::component('settings-tab-security', Security::class);\n\n        return $this;\n    }\n\n    protected function bootRoutes(): static\n    {\n        if (! $this->app->routesAreCached()) {\n            Route::middleware(['web', 'auth'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/web.php'));\n        }\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/settings/testbench.yaml",
    "content": "providers:\n  - Vigilant\\Settings\\ServiceProvider\n"
  },
  {
    "path": "packages/settings/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\Settings\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Livewire\\LivewireServiceProvider;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Settings\\ServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        TeamService::fake();\n    }\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n            \\Vigilant\\Users\\ServiceProvider::class,\n            LivewireServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/sites/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/sites/composer.json",
    "content": "{\n    \"name\": \"vigilant/sites\",\n    \"description\": \"Vigilant Sites\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"laravel/framework\": \"^12.0\",\n        \"livewire/livewire\": \"^3.4\",\n        \"vigilant/core\": \"@dev\",\n        \"vigilant/notifications\": \"@dev\",\n        \"vigilant/lighthouse\": \"@dev\",\n        \"vigilant/dns\": \"@dev\",\n        \"vigilant/uptime\": \"@dev\",\n        \"vigilant/crawler\": \"@dev\",\n        \"vigilant/certificates\": \"@dev\",\n        \"vigilant/healthchecks\": \"@dev\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Sites\\\\\": \"src\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Sites\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\Sites\\\\ServiceProvider\"\n            ]\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/sites/config/sites.php",
    "content": "<?php\n\nreturn [\n    'queue' => 'default',\n];\n"
  },
  {
    "path": "packages/sites/database/migrations/2024_02_20_170000_create_sites_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('sites', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(Team::class)->index();\n\n            $table->string('url');\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('sites');\n    }\n};\n"
  },
  {
    "path": "packages/sites/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n    ignoreErrors:\n        - '#return type with generic class#'\n"
  },
  {
    "path": "packages/sites/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/sites/resources/navigation.php",
    "content": "<?php\n\nuse Vigilant\\Core\\Facades\\Navigation;\n\nNavigation::add(route('sites'), 'Sites')\n    ->routeIs('site*')\n    ->icon('tni-hd-screen-o')\n    ->sort(100);\n"
  },
  {
    "path": "packages/sites/resources/views/components/empty-states/no-monitors.blade.php",
    "content": "@props(['site'])\n\n<x-frontend::empty-state\n    :title=\"__('No Monitors Configured')\"\n    :description=\"__('Get started by adding monitors for this site')\"\n    icon=\"tni-folder-plus-o\"\n    iconClass=\"h-12 w-12 text-red\"\n    iconWrapperClass=\"rounded-full bg-red/10 p-4 mb-6\"\n    :buttonHref=\"route('site.edit', ['site' => $site])\"\n    :buttonText=\"__('Configure Monitors')\"\n    buttonClass=\"bg-gradient-to-r from-red via-orange to-red bg-300% hover:shadow-lg hover:shadow-red/30 transition-all duration-300\"\n/>\n"
  },
  {
    "path": "packages/sites/resources/views/components/site-card.blade.php",
    "content": "@props(['site'])\n\n@php\n    use Vigilant\\Lighthouse\\Livewire\\Tables\\LighthouseMonitorsTable;\n    use Vigilant\\Uptime\\Actions\\CalculateUptimePercentage;\n    use Vigilant\\Frontend\\Integrations\\Table\\Enums\\Status;\n\n    /** @var \\Vigilant\\Sites\\Models\\Site $site */\n\n    // Calculate uptime\n    $calculateUptime = app(CalculateUptimePercentage::class);\n    $uptimeMonitor = $site->uptimeMonitor;\n    $uptimePercentage = $uptimeMonitor ? $calculateUptime->calculate($uptimeMonitor) : null;\n\n    // Get Lighthouse score\n    $lighthouseMonitor = $site->lighthouseMonitors()->first();\n    $lighthouseResult = $lighthouseMonitor?->lighthouseResults()->orderByDesc('id')->first();\n    $lighthouseScore = null;\n    if ($lighthouseResult) {\n        $scores = [\n            $lighthouseResult->performance,\n            $lighthouseResult->accessibility,\n            $lighthouseResult->best_practices,\n            $lighthouseResult->seo,\n        ];\n        $lighthouseScore = array_sum($scores) / count($scores);\n    }\n\n    // Get last downtime\n    $lastDowntime = null;\n    if ($uptimeMonitor) {\n        $lastDowntime = $uptimeMonitor->downtimes()->whereNotNull('end')->orderByDesc('start')->first();\n    }\n\n    // Get link issues\n    $crawler = $site->crawler;\n    $issueCount = $crawler?->issueCount() ?? 0;\n    $totalUrlCount = $crawler?->totalUrlCount() ?? 0;\n    $issueStatus = null;\n    if ($crawler) {\n        if ($issueCount === 0) {\n            $issueStatus = Status::Success;\n        } else {\n            $threshold = $totalUrlCount * 0.05;\n            $issueStatus = $issueCount > $threshold ? Status::Danger : Status::Warning;\n        }\n    }\n\n    // Get certificate info\n    $certificate = $site->certificateMonitor;\n    $certificateStatus = null;\n    if ($certificate && $certificate->valid_to) {\n        $diff = now()->diffInDays($certificate->valid_to);\n        if ($diff > 30) {\n            $certificateStatus = Status::Success;\n        } elseif ($diff > 7) {\n            $certificateStatus = Status::Warning;\n        } else {\n            $certificateStatus = Status::Danger;\n        }\n    }\n\n    // Get healthcheck info\n    $healthcheck = $site->healthcheck;\n    $healthcheckStatus = null;\n    if ($healthcheck) {\n        $healthcheckStatus = match($healthcheck->status?->value ?? null) {\n            'healthy' => Status::Success,\n            'warning' => Status::Warning,\n            'unhealthy' => Status::Danger,\n            default => null,\n        };\n    }\n@endphp\n\n<a href=\"{{ route('site.view', ['site' => $site]) }}\" wire:navigate.hover class=\"block group relative\">\n    <div\n        class=\"border border-base-700 shadow-xl rounded-xl overflow-hidden backdrop-blur-sm relative transition-all duration-300 hover:shadow-2xl hover:shadow-indigo/10 hover:border-base-600\">\n        <!-- Multi-stop gradient background to prevent banding -->\n        <div class=\"absolute inset-0 -z-10\"\n            style=\"background:\n                    linear-gradient(135deg,\n                        rgba(35, 35, 51, 1) 0%,\n                        rgba(33, 33, 48, 1) 10%,\n                        rgba(31, 31, 45, 1) 20%,\n                        rgba(29, 29, 42, 1) 30%,\n                        rgba(28, 28, 40, 1) 40%,\n                        rgba(27, 27, 38, 1) 50%,\n                        rgba(26, 26, 36, 1) 60%,\n                        rgba(26, 26, 36, 1) 70%,\n                        rgba(26, 26, 36, 1) 80%,\n                        rgba(26, 26, 36, 1) 90%,\n                        rgba(26, 26, 36, 1) 100%\n                    ),\n                    url('data:image/svg+xml,%3Csvg viewBox=%220 0 400 400%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noiseFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%221.5%22 numOctaves=%225%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noiseFilter)%22 opacity=%220.12%22/%3E%3C/svg%3E');\">\n        </div>\n\n        <!-- Subtle gradient overlay for depth with noise -->\n        <div class=\"absolute inset-0 pointer-events-none\"\n            style=\"background:\n                    linear-gradient(180deg,\n                        rgba(45, 45, 66, 0.1) 0%,\n                        rgba(45, 45, 66, 0.075) 10%,\n                        rgba(45, 45, 66, 0.05) 20%,\n                        rgba(45, 45, 66, 0.025) 30%,\n                        rgba(45, 45, 66, 0.01) 40%,\n                        transparent 50%\n                    ),\n                    url('data:image/svg+xml,%3Csvg viewBox=%220 0 300 300%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22grainFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%221%22 numOctaves=%224%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23grainFilter)%22 opacity=%220.08%22/%3E%3C/svg%3E');\">\n        </div>\n\n        <!-- Hover glow effect -->\n        <div class=\"absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none\"\n            style=\"background: radial-gradient(800px circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(99, 102, 241, 0.05), transparent 40%);\">\n        </div>\n\n        <div class=\"relative p-6\">\n            <div class=\"flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6\">\n                <!-- Left side: Site URL and metrics -->\n                <div class=\"flex-1 min-w-0\">\n                    <!-- Site URL Header -->\n                    <div class=\"mb-6\">\n                        <h3\n                            class=\"text-xl font-semibold text-base-50 group-hover:text-indigo-light transition-colors duration-200 truncate\">\n                            {{ $site->url }}\n                        </h3>\n                        <div\n                            class=\"mt-2 h-0.5 w-16 bg-gradient-to-r from-indigo to-purple-light rounded-full group-hover:w-full transition-all duration-500\">\n                        </div>\n                    </div>\n\n                    <!-- Metrics Grid -->\n                    <div class=\"grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-6\">\n                        <!-- Lighthouse Score -->\n                        <div class=\"space-y-2\">\n                            <div class=\"flex items-center gap-2\">\n                                <svg class=\"w-4 h-4 text-base-400\" fill=\"none\" stroke=\"currentColor\"\n                                    viewBox=\"0 0 24 24\">\n                                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n                                        d=\"M13 10V3L4 14h7v7l9-11h-7z\"></path>\n                                </svg>\n                                <span\n                                    class=\"text-xs uppercase tracking-wider text-base-400 font-medium\">Lighthouse</span>\n                            </div>\n                            <div class=\"text-base-100\">\n                                @if ($lighthouseScore !== null)\n                                    {!! LighthouseMonitorsTable::scoreDisplay($lighthouseScore) !!}\n                                @elseif ($lighthouseResult === null && $lighthouseMonitor !== null)\n                                    <span class=\"text-sm text-base-400\">{{ __('No Results') }}</span>\n                                @else\n                                    <span class=\"text-base-500 text-2xl\">&mdash;</span>\n                                @endif\n                            </div>\n                        </div>\n\n                        <!-- Uptime -->\n                        <div class=\"space-y-2\">\n                            <div class=\"flex items-center gap-2\">\n                                <svg class=\"w-4 h-4 text-base-400\" fill=\"none\" stroke=\"currentColor\"\n                                    viewBox=\"0 0 24 24\">\n                                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n                                        d=\"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n                                </svg>\n                                <span class=\"text-xs uppercase tracking-wider text-base-400 font-medium\">Uptime</span>\n                            </div>\n                            <div>\n                                @if ($uptimePercentage !== null)\n                                    @php\n                                        $uptimeClass = match (true) {\n                                            $uptimePercentage > 95 => 'text-green-light',\n                                            $uptimePercentage > 80 => 'text-orange',\n                                            default => 'text-red',\n                                        };\n                                    @endphp\n                                    <span class=\"{{ $uptimeClass }} text-2xl font-bold\">{{ $uptimePercentage }}%</span>\n                                @elseif ($uptimeMonitor !== null)\n                                    <span class=\"text-sm text-base-400\">{{ __('Not available yet') }}</span>\n                                @else\n                                    <span class=\"text-base-500 text-2xl\">&mdash;</span>\n                                @endif\n                            </div>\n                        </div>\n\n                        <!-- Last Downtime -->\n                        @if ($uptimeMonitor !== null)\n                            <div class=\"space-y-2\">\n                                <div class=\"flex items-center gap-2\">\n                                    <svg class=\"w-4 h-4 text-base-400\" fill=\"none\" stroke=\"currentColor\"\n                                        viewBox=\"0 0 24 24\">\n                                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n                                            d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n                                    </svg>\n                                    <span class=\"text-xs uppercase tracking-wider text-base-400 font-medium\">Last\n                                        downtime</span>\n                                </div>\n                                <div class=\"text-base-200 text-sm font-medium\">\n                                    @if ($lastDowntime !== null)\n                                        {{ teamTimezone($lastDowntime->start)->diffForHumans() }}\n                                    @else\n                                        {{ __('Never') }}\n                                    @endif\n                                </div>\n                            </div>\n                        @endif\n\n                        <!-- Link Issues -->\n                        @if ($crawler !== null)\n                            <div class=\"space-y-2\">\n                                <div class=\"flex items-center gap-2\">\n                                    <svg class=\"w-4 h-4 text-base-400\" fill=\"none\" stroke=\"currentColor\"\n                                        viewBox=\"0 0 24 24\">\n                                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n                                            d=\"M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1\">\n                                        </path>\n                                    </svg>\n                                    <span class=\"text-xs uppercase tracking-wider text-base-400 font-medium\">Link\n                                        Issues</span>\n                                </div>\n                                <div class=\"flex items-center gap-2\">\n                                    <div class=\"flex-none rounded-full p-1 bg-base-800/50\">\n                                        @if ($issueStatus === Status::Success)\n                                            <div class=\"h-2 w-2 rounded-full bg-green-light\"></div>\n                                        @elseif($issueStatus === Status::Warning)\n                                            <div class=\"h-2 w-2 rounded-full bg-orange-light animate-pulse\"></div>\n                                        @else\n                                            <div class=\"h-2 w-2 rounded-full bg-red-light animate-pulse\"></div>\n                                        @endif\n                                    </div>\n                                    <span class=\"text-sm font-medium text-base-200\">\n                                        {{ __(':count issues', ['count' => $issueCount]) }}\n                                    </span>\n                                </div>\n                            </div>\n                        @endif\n\n                        <!-- Certificate -->\n                        @if ($certificate !== null && $certificate->valid_to !== null)\n                            <div class=\"space-y-2\">\n                                <div class=\"flex items-center gap-2\">\n                                    <svg class=\"w-4 h-4 text-base-400\" fill=\"none\" stroke=\"currentColor\"\n                                        viewBox=\"0 0 24 24\">\n                                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n                                            d=\"M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n                                    </svg>\n                                    <span class=\"text-xs uppercase tracking-wider text-base-400 font-medium\">Certificate</span>\n                                </div>\n                                <div class=\"flex items-center gap-2\">\n                                    <div class=\"flex-none rounded-full p-1 bg-base-800/50\">\n                                        @if ($certificateStatus === Status::Success)\n                                            <div class=\"h-2 w-2 rounded-full bg-green-light\"></div>\n                                        @elseif($certificateStatus === Status::Warning)\n                                            <div class=\"h-2 w-2 rounded-full bg-orange-light animate-pulse\"></div>\n                                        @else\n                                            <div class=\"h-2 w-2 rounded-full bg-red-light animate-pulse\"></div>\n                                        @endif\n                                    </div>\n                                    <span class=\"text-sm font-medium text-base-200\">\n                                        {{ __('Expires in :diff', ['diff' => teamTimezone($certificate->valid_to)->shortAbsoluteDiffForHumans()]) }}\n                                    </span>\n                                </div>\n                            </div>\n                        @endif\n\n                    </div>\n                </div>\n\n                <!-- Right side: Status indicators and action -->\n                <div class=\"flex flex-col items-end gap-4 lg:min-w-[280px]\">\n                    <!-- Healthcheck -->\n                    @if ($healthcheck !== null)\n                        <div\n                            class=\"flex items-center gap-3 bg-base-800/30 rounded-lg px-4 py-3 border border-base-700/50 w-full\">\n                            <div class=\"flex-none rounded-full p-1 bg-base-800/50\">\n                                @if ($healthcheckStatus === Status::Success)\n                                    <div class=\"h-2 w-2 rounded-full bg-green-light\"></div>\n                                @elseif($healthcheckStatus === Status::Warning)\n                                    <div class=\"h-2 w-2 rounded-full bg-orange-light animate-pulse\"></div>\n                                @elseif($healthcheckStatus === Status::Danger)\n                                    <div class=\"h-2 w-2 rounded-full bg-red-light animate-pulse\"></div>\n                                @else\n                                    <div class=\"h-2 w-2 rounded-full bg-base-500\"></div>\n                                @endif\n                            </div>\n                            <div class=\"flex-1 min-w-0\">\n                                <div class=\"text-xs text-base-400 uppercase tracking-wider\">Health</div>\n                                <div class=\"text-sm font-medium text-base-200 truncate\">\n                                    @if ($healthcheck->status)\n                                        {{ ucfirst($healthcheck->status->value) }}\n                                    @else\n                                        {{ __('Unknown') }}\n                                    @endif\n                                </div>\n                            </div>\n                        </div>\n                    @endif\n\n                    <!-- View details button -->\n                    <div\n                        class=\"flex items-center gap-2 text-indigo group-hover:text-indigo-light transition-colors duration-200\">\n                        <span class=\"text-sm font-medium\">{{ __('View details') }}</span>\n                        <svg class=\"w-5 h-5 group-hover:translate-x-1 transition-transform duration-200\" fill=\"none\"\n                            stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\">\n                            </path>\n                        </svg>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</a>\n"
  },
  {
    "path": "packages/sites/resources/views/livewire/form.blade.php",
    "content": "<div>\n    @if (!$inline)\n        <x-slot name=\"header\">\n            <x-page-header :title=\"$updating ? 'Edit Site ' . $site->url : 'Add Site'\" :back=\"$updating ? route('site.view', ['site' => $site]) : route('sites')\"></x-page-header>\n        </x-slot>\n    @endif\n\n    <form wire:submit=\"save\">\n        <div class=\"max-w-7xl mx-auto\">\n            <x-card>\n                <div class=\"flex flex-col gap-4\">\n                    <x-form.text class=\"sm:col-span-2\" field=\"form.url\" name=\"URL\" :disabled=\"$updating\"\n                        description=\"The URL of the site that you want to add\" placeholder=\"{{ config('app.url') }}\" />\n\n                    @if ($updating)\n                        <div x-data=\"{ selectedTab: '{{ \\Illuminate\\Support\\Arr::first(array_keys($tabs)) }}' }\">\n                            <div class=\"mb-4\">\n                                <div class=\"sm:hidden\">\n                                    <label for=\"tabs\" class=\"sr-only\">{{ __('Select a tab') }}</label>\n                                    <select name=\"tabs\" x-model=\"selectedTab\"\n                                        class=\"block w-full rounded-lg border border-base-700 bg-base-850 py-2.5 pl-3 pr-10 text-base-100 shadow-sm focus:border-red focus:ring-2 focus:ring-red/20 transition-colors\">\n                                        @foreach ($tabs as $key => $data)\n                                            <option value=\"{{ $key }}\">{{ $data['title'] }}</option>\n                                        @endforeach\n                                    </select>\n                                </div>\n                                <div class=\"hidden sm:block\">\n                                    <nav class=\"flex gap-2 p-1 bg-base-850/50 rounded-lg border border-base-700/50 backdrop-blur-sm\">\n                                        @foreach ($tabs as $key => $data)\n                                            <button\n                                                type=\"button\"\n                                                x-on:click=\"selectedTab = '{{ $key }}'\"\n                                                :class=\"selectedTab == '{{ $key }}' \n                                                    ? 'bg-gradient-to-r from-red to-orange text-white shadow-lg shadow-red/20' \n                                                    : 'text-base-300 hover:text-base-100 hover:bg-base-800/50'\"\n                                                class=\"flex-1 px-4 py-2.5 text-sm font-medium rounded-md transition-all duration-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red\">\n                                                {{ $data['title'] }}\n                                            </button>\n                                        @endforeach\n                                    </nav>\n                                </div>\n                            </div>\n\n                            <div>\n                                @foreach ($tabs as $key => $data)\n                                    <div x-show=\"selectedTab == '{{ $key }}'\"\n                                         x-transition:enter=\"transition ease-out duration-200\"\n                                         x-transition:enter-start=\"opacity-0 translate-y-2\"\n                                         x-transition:enter-end=\"opacity-100 translate-y-0\">\n                                        <livewire:dynamic-component :is=\"$data['component']\" :site=\"$site\"\n                                            wire:key=\"{{ $key }}\" />\n                                    </div>\n                                @endforeach\n                            </div>\n                        </div>\n                    @endif\n\n                    @if (!$inline)\n                        <x-form.submit-button dusk=\"submit-button\" :submitText=\"$updating ? 'Save' : 'Create'\" />\n                    @endif\n                </div>\n            </x-card>\n        </div>\n    </form>\n</div>\n"
  },
  {
    "path": "packages/sites/resources/views/livewire/import.blade.php",
    "content": "<div>\n    @if (!$inline)\n        <x-slot name=\"header\">\n            <x-page-header title=\"Import\" :back=\"route('sites')\">\n            </x-page-header>\n        </x-slot>\n    @endif\n\n    <div class=\"max-w-7xl mx-auto\">\n        <x-card>\n            <div class=\"flex flex-col gap-6\">\n                @if ($validatedDomains === [])\n                    <form wire:submit.prevent=\"confirm\">\n                        <!-- Domains Input -->\n                        <div class=\"space-y-2 mb-6\">\n                            <div class=\"flex items-center justify-between\">\n                                <div>\n                                    <label for=\"urls\" class=\"block text-base font-semibold leading-6 text-base-100\">\n                                        @lang('Your Websites')\n                                    </label>\n                                    <span class=\"text-base-400 text-sm mt-1\">@lang('Add domains or URLs to import, one per line')</span>\n                                </div>\n                            </div>\n                            <div class=\"mt-3\">\n                                <div class=\"flex rounded-lg bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red transition-all duration-200\">\n                                    <textarea \n                                        name=\"urls\" \n                                        id=\"urls\" \n                                        wire:model=\"urls\" \n                                        wire:loading.attr=\"disabled\" \n                                        rows=\"8\"\n                                        placeholder=\"example.com&#10;https://mysite.com&#10;anotherdomain.org\"\n                                        class=\"flex-1 border-0 bg-transparent py-3 px-4 text-base-100 placeholder:text-base-600 focus:ring-0 sm:text-sm sm:leading-6 disabled:bg-base-950 disabled:text-base-500\"></textarea>\n                                </div>\n\n                                @error('urls')\n                                    <span class=\"text-red text-sm mt-2 block\">{{ $message }}</span>\n                                @enderror\n                            </div>\n                        </div>\n\n                        <!-- Monitor Selection -->\n                        <div class=\"space-y-4 mb-6\">\n                            <div>\n                                <h3 class=\"text-base font-semibold text-base-100 mb-1\">@lang('Monitoring Features')</h3>\n                                <p class=\"text-sm text-base-400\">@lang('Select which monitors to enable for your imported sites')</p>\n                            </div>\n                            \n                            <div class=\"grid grid-cols-1 md:grid-cols-2 gap-3\">\n                                @foreach ($availableMonitors as $key => $monitor)\n                                    <label \n                                        x-data=\"{ enabled: $wire.entangle('monitors.{{ $key }}').live }\"\n                                        :class=\"enabled ? 'border-blue bg-base-850/50' : 'border-base-700 bg-base-850'\"\n                                        class=\"relative flex items-start gap-4 p-4 rounded-lg border hover:border-base-600 transition-all duration-200 cursor-pointer group\">\n                                        <button type=\"button\"\n                                            role=\"switch\"\n                                            x-on:click=\"enabled = !enabled\"\n                                            :aria-checked=\"enabled.toString()\"\n                                            :class=\"enabled ? 'bg-gradient-to-r from-red to-orange' : 'bg-base-700'\"\n                                            class=\"relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue focus:ring-offset-2 focus:ring-offset-base-900 mt-0.5\">\n                                            <span class=\"sr-only\">{{ $monitor['label'] }}</span>\n                                            <span :class=\"enabled ? 'translate-x-5' : 'translate-x-0'\"\n                                                  class=\"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-base-50 shadow-lg ring-0 transition duration-200 ease-in-out\"></span>\n                                        </button>\n                                        <div class=\"flex-1 min-w-0\">\n                                            <span class=\"text-base-100 font-semibold block leading-tight\">{{ $monitor['label'] }}</span>\n                                            <span class=\"text-xs text-base-400 block mt-1 leading-relaxed\">{{ $monitor['description'] }}</span>\n                                        </div>\n                                    </label>\n                                @endforeach\n                            </div>\n\n                            @error('monitors')\n                                <span class=\"text-red text-sm\">{{ $message }}</span>\n                            @enderror\n                        </div>\n\n                        <!-- Submit Button -->\n                        <div class=\"flex justify-end\">\n                            <button type=\"submit\" \n                                wire:loading.attr=\"disabled\"\n                                wire:loading.class=\"opacity-50 cursor-not-allowed\"\n                                class=\"inline-flex items-center px-6 py-3 bg-gradient-to-r from-red to-orange hover:from-red-light hover:to-orange-light text-base-100 font-semibold rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red focus:ring-offset-base-900 shadow-lg hover:shadow-red/30\">\n                                <span wire:loading.remove wire:target=\"confirm\">@lang('Continue') →</span>\n                                <span wire:loading wire:target=\"confirm\">@lang('Processing...')</span>\n                            </button>\n                        </div>\n                    </form>\n                @else\n                    <!-- Confirmation Step -->\n                    <div class=\"text-base-100 space-y-6\">\n                        <div>\n                            <h3 class=\"text-xl font-bold text-base-100 mb-2\">\n                                @lang('Ready to import :count sites', ['count' => count($validatedDomains)])\n                            </h3>\n                            <p class=\"text-base-400\">@lang('Review the domains below before importing')</p>\n                        </div>\n                        \n                        <div class=\"bg-base-850 border border-base-700 rounded-lg p-4 max-h-[400px] overflow-y-auto\">\n                            <ul class=\"space-y-2\">\n                                @foreach ($validatedDomains as $domain)\n                                    <li class=\"flex items-center gap-2 text-base-200\">\n                                        <svg class=\"w-5 h-5 text-green\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                                            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\"></path>\n                                        </svg>\n                                        <span>{{ $domain }}</span>\n                                    </li>\n                                @endforeach\n                            </ul>\n                        </div>\n                        \n                        <div class=\"flex justify-end gap-3\">\n                            <button type=\"button\"\n                                wire:click=\"cancel\"\n                                wire:loading.attr=\"disabled\"\n                                class=\"inline-flex items-center px-6 py-3 bg-base-800 hover:bg-base-700 text-base-100 font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-base-700 focus:ring-offset-base-900\">\n                                @lang('← Back')\n                            </button>\n                            <button type=\"button\"\n                                wire:click=\"import\"\n                                wire:loading.attr=\"disabled\"\n                                wire:loading.class=\"opacity-50 cursor-not-allowed\"\n                                class=\"inline-flex items-center px-6 py-3 bg-gradient-to-r from-red to-orange hover:from-red-light hover:to-orange-light text-base-100 font-semibold rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red focus:ring-offset-base-900 shadow-lg hover:shadow-red/30\">\n                                <span wire:loading.remove wire:target=\"import\">@lang('Import Sites') ✓</span>\n                                <span wire:loading wire:target=\"import\">@lang('Importing...')</span>\n                            </button>\n                        </div>\n                    </div>\n                @endif\n            </div>\n        </x-card>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/sites/resources/views/livewire/sites.blade.php",
    "content": "<div>\n    <x-slot name=\"header\">\n        <x-page-header title=\"Sites\">\n            <x-frontend::page-header.actions>\n                <x-create-button dusk=\"site-import-button\" :href=\"route('site.import')\" model=\"Vigilant\\Sites\\Models\\Site\">\n                    @lang('Add Multiple Sites')\n                </x-create-button>\n                <x-create-button dusk=\"site-add-button\" :href=\"route('site.create')\" model=\"Vigilant\\Sites\\Models\\Site\">\n                    @lang('Add Site')\n                </x-create-button>\n\n            </x-frontend::page-header.actions>\n            <x-frontend::page-header.mobile-actions>\n                <x-create-button-dropdown :href=\"route('site.create')\" model=\"Vigilant\\Sites\\Models\\Site\">\n                    @lang('Add Site')\n                </x-create-button-dropdown>\n                <x-create-button-dropdown :href=\"route('site.import')\" model=\"Vigilant\\Sites\\Models\\Site\">\n                    @lang('Add Multiple Sites')\n                </x-create-button-dropdown>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    <div class=\"mx-auto max-w-7xl px-6 lg:px-8 py-8\">\n        @if ($sites->count() > 0)\n            <div class=\"space-y-4\">\n                @foreach ($sites as $site)\n                    <x-sites::site-card :site=\"$site\" />\n                @endforeach\n            </div>\n\n            @if ($sites->hasPages())\n                <div class=\"mt-8\">\n                    {{ $sites->links() }}\n                </div>\n            @endif\n        @else\n            <div class=\"text-center py-12\">\n                <svg class=\"mx-auto h-12 w-12 text-base-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9\"></path>\n                </svg>\n                <h3 class=\"mt-4 text-lg font-semibold text-base-100\">{{ __('No sites yet') }}</h3>\n                <p class=\"mt-2 text-sm text-base-300\">{{ __('Get started by adding your first site.') }}</p>\n                <div class=\"mt-6\">\n                    <x-create-button :href=\"route('site.create')\" model=\"Vigilant\\Sites\\Models\\Site\">\n                        @lang('Add Site')\n                    </x-create-button>\n                </div>\n            </div>\n        @endif\n    </div>\n\n</div>\n"
  },
  {
    "path": "packages/sites/resources/views/livewire/tabs/certificate-monitor.blade.php",
    "content": "<div>\n\n    <div class=\"mb-4\">\n        <x-form.checkbox name=\"Enable Certificate Monitoring\" description=\"Enable certificate monitoring for this site\"\n            field=\"enabled\" dusk=\"certificate-tab-enabled\"></x-form.checkbox>\n    </div>\n\n    @if ($enabled)\n        <livewire:certificate-monitor-form :monitor=\"$this->monitor\" :inline=\"true\" />\n    @endif\n\n</div>\n"
  },
  {
    "path": "packages/sites/resources/views/livewire/tabs/crawler.blade.php",
    "content": "<div>\n    <div class=\"mb-4\">\n        <x-form.checkbox\n            name=\"Enable Link Crawling\"\n            description=\"Crawl your website to find broken links\"\n            field=\"enabled\"\n            dusk=\"crawler-tab-enabled\"\n        ></x-form.checkbox>\n    </div>\n\n    @if($enabled)\n        <livewire:crawler-form\n            :crawler=\"$this->crawler\"\n            :inline=\"true\"\n            :siteId=\"$siteId\"\n        />\n    @endif\n</div>\n"
  },
  {
    "path": "packages/sites/resources/views/livewire/tabs/dns-monitors.blade.php",
    "content": "<div>\n    <livewire:dns-monitor-import :inline=\"true\" :siteId=\"$siteId\"/>\n</div>\n"
  },
  {
    "path": "packages/sites/resources/views/livewire/tabs/healthcheck-monitor.blade.php",
    "content": "<div>\n\n    <div class=\"mb-4\">\n        <x-form.checkbox\n            name=\"Enable Healthcheck Monitoring\"\n            description=\"Enable healthcheck monitoring for this site\"\n            field=\"enabled\"\n            dusk=\"healthcheck-tab-enabled\"\n        ></x-form.checkbox>\n    </div>\n\n    @if($enabled)\n        <livewire:healthcheck-form\n            :healthcheck=\"$this->healthcheck\"\n            :inline=\"true\"\n        />\n    @endif\n\n</div>\n"
  },
  {
    "path": "packages/sites/resources/views/livewire/tabs/lighthouse-monitor.blade.php",
    "content": "<div>\n\n    <div class=\"mb-4\">\n        <h2 class=\"text-lg text-base-100\">@lang('Pages')</h2>\n        <p class=\"text-sm text-base-200\">@lang('Select which pages have to be monitored by Lighthouse. You may add multiple URLs.')</p>\n    </div>\n\n    <div class=\"grid grid-cols-3 space-y-2\">\n\n        <h3 class=\"text-md text-base-200 font-bold\">@lang('URL')</h3>\n        <h3 class=\"text-md text-base-200 font-bold\">@lang('Interval')</h3>\n        <span></span>\n\n\n        @foreach ($monitors as $index => $monitor)\n            <div class=\"pr-4\">\n                <input type=\"text\" wire:model.live=\"monitors.{{ $index }}.url\"\n                    class=\"mt-2 w-full block rounded-md border-0 py-1.5 pl-3 pr-10 text-white bg-white/5 ring-1 ring-inset ring-white/10 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red\">\n                @error(\"monitors.$index.url\")\n                    <span class=\"text-red\">{{ $message }}</span>\n                @enderror\n            </div>\n            <div>\n                <select wire:model.live=\"monitors.{{ $index }}.interval\"\n                    class=\"mt-2 block rounded-md border-0 py-1.5 pl-3 pr-10 text-white bg-white/5 ring-1 ring-inset ring-white/10 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red\">\n                    @foreach (config('lighthouse.intervals') as $interval => $label)\n                        <option value=\"{{ $interval }}\">@lang($label)</option>\n                    @endforeach\n                </select>\n            </div>\n\n            @if (!blank($monitor['id'] ?? null))\n                <div class=\"flex justify-end items-center\">\n                    <x-form.button href=\"{{ route('lighthouse.index', ['monitor' => $monitor['id']]) }}\" target=\"_blank\"\n                        class=\"bg-green\">@lang('View')</x-form.button>\n                </div>\n            @else\n                <span></span>\n            @endif\n        @endforeach\n\n    </div>\n\n    <div class=\"mt-4\">\n        <x-form.button type=\"button\" wire:click=\"addPage\" class=\"bg-gradient-to-r from-red via-orange to-red\">@lang('Add Lighthouse Monitor')</x-form.button>\n    </div>\n\n</div>\n"
  },
  {
    "path": "packages/sites/resources/views/livewire/tabs/uptime-monitor.blade.php",
    "content": "<div>\n\n    <div class=\"mb-4\">\n        <x-form.checkbox\n            name=\"Enable Uptime Monitoring\"\n            description=\"Enable uptime monitoring for this site\"\n            field=\"enabled\"\n            dusk=\"uptime-tab-enabled\"\n        ></x-form.checkbox>\n    </div>\n\n    @if($enabled)\n        <livewire:uptime-monitor-form\n            :monitor=\"$this->monitor\"\n            :inline=\"true\"\n        />\n    @endif\n\n</div>\n"
  },
  {
    "path": "packages/sites/resources/views/sites/view.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header :back=\"route('sites')\" title=\"Site - {{ $site->url }}\">\n            <x-frontend::page-header.actions>\n                <x-form.button dusk=\"site-edit-button\" :href=\"route('site.edit', ['site' => $site])\">\n                    @lang('Edit')\n                </x-form.button>\n                <x-form.button class=\"bg-red\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.button>\n            </x-frontend::page-header.actions>\n\n            <x-frontend::page-header.mobile-actions>\n                <x-form.dropdown-button dusk=\"site-edit-button\" :href=\"route('site.edit', ['site' => $site])\">\n                    @lang('Edit')\n                </x-form.dropdown-button>\n                <x-form.dropdown-button class=\"!text-red hover:!text-red-light\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.dropdown-button>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    @if ($empty)\n        <x-sites::empty-states.no-monitors :site=\"$site\" />\n    @else\n        <x-frontend::tabs.container :activeTab=\"!empty($tabs) ? $tabs[0]['key'] : ''\" class=\"pb-12\">\n            \n            {{-- Tab Navigation --}}\n            <x-frontend::tabs.navigation :tabs=\"$tabs\" />\n\n            {{-- Tab Panels --}}\n            <div class=\"space-y-6\">\n                @foreach($tabs as $index => $tab)\n                    @if(!isset($tab['gate']) || auth()->user()->can($tab['gate']))\n                        <x-frontend::tabs.panel :key=\"$tab['key']\" :cloak=\"$index !== 0\">\n                            \n                            {{-- Section Header with Link --}}\n                            <div class=\"flex items-center justify-between\">\n                                <div>\n                                    <h2 class=\"text-2xl font-bold text-base-50 flex items-center gap-3\">\n                                        @svg($tab['icon'], 'size-6 text-' . $tab['color'])\n                                        {{ $tab['title'] }}\n                                    </h2>\n                                    <p class=\"text-base-300 mt-1\">{{ $tab['description'] }}</p>\n                                </div>\n                                <a href=\"{{ $tab['route'] }}\" \n                                   class=\"group flex items-center gap-2 px-4 py-2.5 rounded-lg border-2 border-base-700 bg-base-850/50 hover:border-{{ $tab['color'] }} hover:bg-base-800/50 text-base-200 hover:text-base-50 transition-all duration-300 text-sm font-medium\">\n                                    <span>@lang('View Details')</span>\n                                    @svg('tni-right-o', 'size-4 group-hover:translate-x-1 transition-transform duration-300')\n                                </a>\n                            </div>\n\n                            <x-frontend::card>\n                                @if($tab['key'] === 'uptime')\n                                    <livewire:monitor-dashboard :monitorId=\"$tab['monitor']->id\" />\n                                @elseif($tab['key'] === 'lighthouse')\n                                    <livewire:lighthouse-monitor-dashboard :monitorId=\"$tab['monitor']->id\" />\n                                @elseif($tab['key'] === 'crawler')\n                                    <livewire:crawler-dashboard :crawlerId=\"$tab['monitor']->id\" wire:key=\"{{ $tab['componentKey'] }}\" />\n                                @elseif($tab['key'] === 'dns')\n                                    <livewire:dns-monitor-dashboard :siteId=\"$tab['monitor']->id\" wire:key=\"{{ $tab['componentKey'] }}\" />\n                                @elseif($tab['key'] === 'certificate')\n                                    <livewire:certificate-monitor-dashboard :monitorId=\"$tab['monitor']->id\" />\n                                @elseif($tab['key'] === 'healthcheck')\n                                    <livewire:healthcheck-dashboard :healthcheckId=\"$tab['monitor']->id\" wire:key=\"{{ $tab['componentKey'] }}\" />\n                                @endif\n                            </x-frontend::card>\n                        </x-frontend::tabs.panel>\n                    @endif\n                @endforeach\n            </div>\n        </x-frontend::tabs.container>\n    @endif\n\n    <!-- Delete Confirmation Modal -->\n    <div x-data=\"{ showDeleteModal: false }\" @open-delete-modal.window=\"showDeleteModal = true\">\n        <x-frontend::modal show=\"showDeleteModal\">\n            <x-frontend::modal.header icon=\"phosphor-trash\" iconColor=\"red\" show=\"showDeleteModal\">\n                @lang('Delete Site')\n            </x-frontend::modal.header>\n\n            <x-frontend::modal.body>\n                <div class=\"space-y-4\">\n                    <p class=\"text-base-100\">\n                        @lang('Are you sure you want to delete this site?')\n                    </p>\n                    <div class=\"bg-base-850 border border-base-700 rounded-lg p-4\">\n                        <div class=\"flex items-start gap-3\">\n                            <div class=\"flex-shrink-0\">\n                                @svg('phosphor-warning-circle', 'w-5 h-5 text-orange mt-0.5')\n                            </div>\n                            <div class=\"flex-1\">\n                                <p class=\"text-sm text-base-300\">\n                                    <span class=\"font-semibold text-base-100\">{{ $site->url }}</span>\n                                </p>\n                                <p class=\"text-sm text-base-400 mt-1\">\n                                    @lang('This action cannot be undone. This will permanently delete the site and all associated monitors (uptime, lighthouse, crawler, etc.).')\n                                </p>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </x-frontend::modal.body>\n\n            <x-frontend::modal.footer>\n                <x-form.button type=\"button\" @click=\"showDeleteModal = false\">\n                    @lang('Cancel')\n                </x-form.button>\n                <form action=\"{{ route('site.delete', ['site' => $site]) }}\" method=\"POST\" class=\"inline\">\n                    @csrf\n                    @method('DELETE')\n                    <x-form.button class=\"bg-red\" type=\"submit\">\n                        @lang('Delete Site')\n                    </x-form.button>\n                </form>\n            </x-frontend::modal.footer>\n        </x-frontend::modal>\n    </div>\n\n</x-app-layout>\n"
  },
  {
    "path": "packages/sites/routes/web.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\Sites\\Http\\Controllers\\SiteController;\nuse Vigilant\\Sites\\Http\\Livewire\\ImportSites;\nuse Vigilant\\Sites\\Http\\Livewire\\SiteForm;\nuse Vigilant\\Sites\\Http\\Livewire\\Sites;\n\nRoute::get('site', Sites::class)->name('sites');\nRoute::get('site/create', SiteForm::class)->name('site.create');\nRoute::get('site/{site}', [SiteController::class, 'view'])->name('site.view');\nRoute::get('site/edit/{site}', SiteForm::class)->name('site.edit');\nRoute::delete('site/{site}', [SiteController::class, 'delete'])->name('site.delete');\nRoute::get('sites/import', ImportSites::class)->name('site.import');\n"
  },
  {
    "path": "packages/sites/src/Actions/ImportSite.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Actions;\n\nuse BlueLibraries\\Dns\\Records\\AbstractRecord;\nuse BlueLibraries\\Dns\\Records\\RecordTypes;\nuse Illuminate\\Support\\Arr;\nuse Illuminate\\Support\\Facades\\Gate;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Crawler\\Enums\\State;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Dns\\Client\\DnsClient;\nuse Vigilant\\Dns\\Enums\\Type as DnsType;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Uptime\\Enums\\Type;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Users\\Models\\User;\n\nclass ImportSite\n{\n    public function __construct(\n        protected TeamService $teamService,\n        protected DnsClient $dnsClient\n    ) {}\n\n    /** @param array<string, bool> $monitors */\n    public function import(int $teamId, string $domain, array $monitors): void\n    {\n        $this->teamService->setTeamById($teamId);\n        $user = User::query()->firstWhere('current_team_id', $teamId);\n\n        throw_if($user === null, 'User not found');\n\n        if (Gate::forUser($user)->denies('create', Site::class)) {\n            return;\n        }\n\n        $site = Site::query()->firstOrCreate(['url' => 'https://'.$domain]);\n\n        if ($monitors['uptime'] ?? false) {\n            $this->importUptime($site);\n        }\n        if ($monitors['lighthouse'] ?? false) {\n            $this->importLighthouse($site);\n        }\n        if ($monitors['dns'] ?? false) {\n            $this->importDns($site);\n        }\n        if ($monitors['certificate'] ?? false) {\n            $this->importCertificate($site);\n        }\n        if ($monitors['crawler'] ?? false) {\n            $this->importCrawler($site);\n        }\n    }\n\n    protected function importUptime(Site $site): void\n    {\n        Monitor::query()->firstOrCreate([\n            'site_id' => $site->id,\n            'team_id' => $site->team_id,\n        ], [\n            'name' => $site->url,\n            'enabled' => true,\n            'type' => Type::Http,\n            'interval' => 60,\n            'retries' => 1,\n            'timeout' => 5,\n            'settings' => [\n                'host' => $site->url,\n            ],\n        ]);\n\n    }\n\n    protected function importLighthouse(Site $site): void\n    {\n        $intervals = array_keys(config()->array('lighthouse.intervals'));\n\n        LighthouseMonitor::query()->firstOrCreate([\n            'site_id' => $site->id,\n            'team_id' => $site->team_id,\n        ], [\n            'url' => $site->url,\n            'interval' => Arr::first($intervals),\n            'settings' => [],\n        ]);\n    }\n\n    protected function importDns(Site $site): void\n    {\n        /** @var array<int, AbstractRecord> $records */\n        $records = $this->dnsClient->get($this->stripProtocol($site->url), [\n            RecordTypes::A,\n            RecordTypes::AAAA,\n            RecordTypes::CNAME,\n            RecordTypes::SOA,\n            RecordTypes::TXT,\n            RecordTypes::MX,\n            RecordTypes::NS,\n        ]);\n\n        foreach ($records as $record) {\n            $data = $record->toArray();\n\n            $type = DnsType::tryFrom($data['type']);\n\n            if ($type === null || ! $type->hasParser()) {\n                continue;\n            }\n\n            $value = $type->parser()->parse($data);\n\n            DnsMonitor::query()->updateOrCreate([\n                'site_id' => $site->id,\n                'team_id' => $site->team_id,\n                'type' => $type,\n                'record' => $record->getHost(),\n            ], [\n                'value' => $value,\n            ]);\n        }\n    }\n\n    protected function importCertificate(Site $site): void\n    {\n        CertificateMonitor::query()->firstOrCreate([\n            'site_id' => $site->id,\n            'team_id' => $site->team_id,\n        ], [\n            'domain' => $this->stripProtocol($site->url),\n            'port' => str_starts_with($site->url, 'https://') ? 443 : 80,\n        ]);\n    }\n\n    protected function importCrawler(Site $site): void\n    {\n        Crawler::query()->firstOrCreate([\n            'site_id' => $site->id,\n            'team_id' => $site->team_id,\n        ], [\n            'state' => State::Pending,\n            'schedule' => '0 10 * * 1',\n            'start_url' => $site->url,\n        ]);\n    }\n\n    protected function stripProtocol(string $domain): string\n    {\n        return preg_replace('#^https?://#', '', $domain) ?? $domain;\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Conditions/SiteCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Conditions;\n\nuse Vigilant\\Notifications\\Conditions\\SelectCondition;\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass SiteCondition extends SelectCondition\n{\n    public static string $name = 'Site';\n\n    /** {@inheritDoc} */\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        if (! is_a($notification, HasSite::class)) {\n            return false;\n        }\n\n        $site = $notification->site();\n\n        if ($site === null) {\n            return false;\n        }\n\n        return match ($operator) {\n            '=' => $site->id == $value,\n            '!=' => $site->id != $value,\n            default => false,\n        };\n    }\n\n    /** {@inheritDoc} */\n    public function operators(): array\n    {\n        return [\n            '=' => 'is',\n            '!=' => 'is not',\n        ];\n    }\n\n    /** {@inheritDoc} */\n    public function options(): array\n    {\n        return Site::query()->get()\n            ->mapWithKeys(fn (Site $site): array => [$site->id => $site->url])\n            ->toArray();\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Http/Controllers/SiteController.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Http\\Controllers;\n\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Routing\\Controller;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass SiteController extends Controller\n{\n    public function view(Site $site): mixed\n    {\n        $monitors = [\n            'uptimeMonitor' => $site->uptimeMonitor,\n            'lighthouseMonitor' => $site->lighthouseMonitors->first(),\n            'crawler' => $site->crawler,\n            'certificateMonitor' => $site->certificateMonitor,\n            'healthcheck' => $site->healthcheck,\n        ];\n\n        // Define tabs configuration\n        $tabs = [];\n\n        if ($site->uptimeMonitor !== null) {\n            $tabs[] = [\n                'key' => 'uptime',\n                'label' => __('Uptime'),\n                'icon' => 'tni-double-caret-up-circle-o',\n                'color' => 'red',\n                'title' => __('Uptime Monitoring'),\n                'description' => __('Monitor your site availability and response times'),\n                'route' => route('uptime.monitor.view', ['monitor' => $site->uptimeMonitor]),\n                'monitor' => $site->uptimeMonitor,\n                'component' => 'monitor-dashboard',\n                'componentKey' => 'uptime-dashboard',\n                'gate' => 'use-uptime',\n            ];\n        }\n\n        if ($site->lighthouseMonitors->first() !== null) {\n            $tabs[] = [\n                'key' => 'lighthouse',\n                'label' => __('Lighthouse'),\n                'icon' => 'phosphor-lighthouse-light',\n                'color' => 'blue',\n                'title' => __('Lighthouse Performance'),\n                'description' => __('Track your site performance, accessibility, and SEO scores'),\n                'route' => route('lighthouse.index', ['monitor' => $site->lighthouseMonitors->first()]),\n                'monitor' => $site->lighthouseMonitors->first(),\n                'component' => 'lighthouse-monitor-dashboard',\n                'componentKey' => 'lighthouse-dashboard',\n            ];\n        }\n\n        if ($site->crawler !== null) {\n            $tabs[] = [\n                'key' => 'crawler',\n                'label' => __('URL Issues'),\n                'icon' => 'carbon-text-link',\n                'color' => 'purple',\n                'title' => __('URL Issues'),\n                'description' => __('Identify broken links and crawl errors on your site'),\n                'route' => route('crawler.view', ['crawler' => $site->crawler]),\n                'monitor' => $site->crawler,\n                'component' => 'crawler-dashboard',\n                'componentKey' => 'crawler-dashboard',\n            ];\n        }\n\n        if ($site->dnsMonitors->count() > 0) {\n            $tabs[] = [\n                'key' => 'dns',\n                'label' => __('DNS Records'),\n                'icon' => 'phosphor-globe-simple',\n                'color' => 'indigo',\n                'title' => __('DNS Records'),\n                'description' => __('Monitor your DNS configuration and record changes'),\n                'route' => route('dns.index'),\n                'monitor' => $site,\n                'component' => 'dns-monitor-dashboard',\n                'componentKey' => 'dns-dashboard',\n            ];\n        }\n\n        if ($site->certificateMonitor !== null) {\n            $tabs[] = [\n                'key' => 'certificate',\n                'label' => __('Certificate'),\n                'icon' => 'phosphor-certificate',\n                'color' => 'green',\n                'title' => __('SSL Certificate'),\n                'description' => __('Monitor SSL certificate validity and expiration dates'),\n                'route' => route('certificates.index', ['monitor' => $site->certificateMonitor]),\n                'monitor' => $site->certificateMonitor,\n                'component' => 'certificate-monitor-dashboard',\n                'componentKey' => 'certificate-dashboard',\n                'gate' => 'use-certificates',\n            ];\n        }\n\n        if ($site->healthcheck !== null) {\n            $tabs[] = [\n                'key' => 'healthcheck',\n                'label' => __('Healthcheck'),\n                'icon' => 'phosphor-heartbeat',\n                'color' => 'teal',\n                'title' => __('Healthcheck Monitoring'),\n                'description' => __('Monitor application health and custom metrics'),\n                'route' => route('healthchecks.view', ['healthcheck' => $site->healthcheck]),\n                'monitor' => $site->healthcheck,\n                'component' => 'healthcheck-dashboard',\n                'componentKey' => 'healthcheck-dashboard',\n                'gate' => 'use-healthchecks',\n            ];\n        }\n\n        $data = array_merge([\n            'site' => $site,\n            'empty' => collect($monitors)->filter()->isEmpty(),\n            'tabs' => $tabs,\n        ], $monitors);\n\n        /** @var view-string $view */\n        $view = 'sites::sites.view';\n\n        return view($view, $data);\n    }\n\n    public function delete(Site $site): RedirectResponse\n    {\n        $site->delete();\n\n        return redirect()->route('sites')->with('success', __('Site deleted successfully.'));\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Http/Livewire/Forms/CreateSiteForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Http\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Form;\n\nclass CreateSiteForm extends Form\n{\n    #[Validate('required|url')]\n    public string $url = '';\n}\n"
  },
  {
    "path": "packages/sites/src/Http/Livewire/ImportSites.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Http\\Livewire;\n\nuse Illuminate\\View\\View;\nuse Livewire\\Component;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Frontend\\Traits\\CanBeInline;\nuse Vigilant\\Sites\\Jobs\\ImportSiteJob;\nuse Vigilant\\Users\\Models\\User;\n\nclass ImportSites extends Component\n{\n    use CanBeInline;\n    use DisplaysAlerts;\n\n    public string $urls = '';\n\n    /** @var array<int, string> */\n    public array $validatedDomains = [];\n\n    /** @var array<string, bool> */\n    public array $monitors = [\n        'uptime' => true,\n        'lighthouse' => true,\n        'dns' => true,\n        'certificate' => true,\n        'crawler' => true,\n    ];\n\n    public function mount(): void\n    {\n        if (session()->has('import_urls')) {\n            $this->urls = implode(PHP_EOL, session()->get('import_urls', []));\n            session()->forget('import_urls');\n        }\n    }\n\n    public function confirm(): void\n    {\n        $this->validatedDomains = str($this->urls)\n            ->explode(\"\\n\")\n            ->map(fn (string $url): string => trim($url))\n            ->map(function (string $url): string {\n                $url = preg_replace('#^https?://#', '', $url) ?? '';\n                $url = preg_replace('#/.*$#', '', $url) ?? '';\n\n                return $url;\n            })\n            ->filter(fn (string $url): bool => ! blank($url))\n            ->filter(fn (string $url): mixed => preg_match('/^(?!:\\/\\/)([a-zA-Z0-9-_]+\\.)+[a-zA-Z]{2,}$/', $url) === 1)\n            ->all();\n\n        $this->urls = implode(PHP_EOL, $this->validatedDomains);\n\n        $this->validate([\n            'urls' => 'required|string',\n            'monitors' => 'array|min:1',\n        ]);\n    }\n\n    public function cancel(): void\n    {\n        $this->validatedDomains = [];\n    }\n\n    public function import(): void\n    {\n        /** @var User $user */\n        $user = auth()->user();\n\n        abort_if($user->current_team_id === null, 403);\n\n        foreach ($this->validatedDomains as $domain) {\n            ImportSiteJob::dispatch(\n                teamId: $user->current_team_id,\n                domain: $domain,\n                monitors: $this->monitors,\n            );\n        }\n\n        if ($this->inline) {\n            $this->dispatch('sites-imported');\n\n            return;\n        }\n\n        $this->alert(\n            __('Saved'),\n            __(':count sites are being imported', ['count' => count($this->validatedDomains)]),\n            AlertType::Success\n        );\n\n        $this->redirectRoute('sites');\n    }\n\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'sites::livewire.import';\n\n        return view($view, [\n            'availableMonitors' => [\n                'uptime' => [\n                    'label' => __('Uptime'),\n                    'description' => __('Track uptime and response times, get notified when your site goes down'),\n                ],\n                'lighthouse' => [\n                    'label' => __('Lighthouse'),\n                    'description' => __('Monitor Google Lighthouse scores'),\n                ],\n                'dns' => [\n                    'label' => __('DNS'),\n                    'description' => __('Track changes in DNS records'),\n                ],\n                'certificate' => [\n                    'label' => __('Certificate'),\n                    'description' => __('Monitor SSL certificate expiration and validity'),\n                ],\n                'crawler' => [\n                    'label' => __('Link Issues'),\n                    'description' => __('Crawl your site to find broken links and errors'),\n                ],\n            ],\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Http/Livewire/SiteForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Http\\Livewire;\n\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\View\\View;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Frontend\\Traits\\CanBeInline;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Sites\\Http\\Livewire\\Forms\\CreateSiteForm;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Uptime\\Models\\Monitor as UptimeMonitor;\n\nclass SiteForm extends Component\n{\n    use CanBeInline;\n    use DisplaysAlerts;\n\n    public CreateSiteForm $form;\n\n    #[Locked]\n    public Site $site;\n\n    public function mount(?Site $site): void\n    {\n        if ($site !== null) {\n            $this->form->fill($site->toArray());\n            $this->site = $site;\n        }\n        \n        if ($domain = request()->query('domain')) {\n            $this->form->fill(['url' => $this->normalizeDomainToUrl($domain)]);\n        }\n    }\n\n    protected function normalizeDomainToUrl(string $domain): string\n    {\n        $domain = strtolower(trim($domain));\n        \n        if (str_starts_with($domain, 'http://') || str_starts_with($domain, 'https://')) {\n            return $domain;\n        }\n        \n        return 'https://' . $domain;\n    }\n\n    #[On('save')]\n    public function save(): void\n    {\n        // Save tabs\n        if ($this->site->exists) {\n            $this->dispatch('save');\n        }\n\n        $this->form->url = $this->normalizeUrl($this->form->url);\n\n        $this->validate();\n\n        if ($this->site->exists) {\n            $this->site->update($this->form->all());\n        } else {\n            $this->site = Site::query()->create(\n                $this->form->all()\n            );\n        }\n\n        if ($this->inline) {\n            $this->dispatch('siteSaved', $this->site->id);\n\n            return;\n        }\n\n        $this->alert(\n            __('Saved'),\n            __('Site was successfully :action', ['action' => $this->site->wasRecentlyCreated ? 'created' : 'saved']),\n            AlertType::Success\n        );\n\n        $this->redirectRoute('site.view', ['site' => $this->site]);\n    }\n\n    public function render(): View\n    {\n        $tabs = collect($this->tabs())\n            ->filter(fn (array $tab) => ! array_key_exists('gate', $tab) || Gate::check($tab['gate']))\n            ->toArray();\n\n        /** @var view-string $view */\n        $view = 'sites::livewire.form';\n\n        return view($view, [\n            'updating' => $this->site->exists,\n            'tabs' => $tabs,\n        ]);\n    }\n\n    private function normalizeUrl(?string $url): string\n    {\n        if ($url === null) {\n            return '';\n        }\n        $parts = parse_url($url);\n\n        if ($parts === false || ! isset($parts['scheme'], $parts['host'])) {\n            return rtrim($url, '/');\n        }\n\n        return sprintf('%s://%s', $parts['scheme'], $parts['host']);\n    }\n\n    /** @return array<string, array<string, string>> */\n    protected function tabs(): array\n    {\n        return [\n            'uptime' => [\n                'title' => __('Uptime Monitoring'),\n                'component' => 'sites.tabs.uptime-monitor',\n                'gate' => 'use-uptime',\n                'model' => UptimeMonitor::class,\n            ],\n\n            'lighthouse' => [\n                'title' => __('Lighthouse Monitoring'),\n                'component' => 'sites.tabs.lighthouse-monitor',\n                'gate' => 'use-lighthouse',\n                'model' => LighthouseMonitor::class,\n            ],\n\n            'dns' => [\n                'title' => __('DNS Monitoring'),\n                'component' => 'sites.tabs.dns-monitors',\n                'gate' => 'use-dns',\n                'model' => DnsMonitor::class,\n            ],\n\n            'crawler' => [\n                'title' => __('Link Issues'),\n                'component' => 'sites.tabs.crawler',\n                'gate' => 'use-crawler',\n                'model' => Crawler::class,\n            ],\n\n            'certificates' => [\n                'title' => __('Certificate Monitoring'),\n                'component' => 'sites.tabs.certificate-monitor',\n                'gate' => 'use-certificates',\n                'model' => CertificateMonitor::class,\n            ],\n\n            'healthcheck' => [\n                'title' => __('Healthcheck Monitoring'),\n                'component' => 'sites.tabs.healthcheck-monitor',\n                'gate' => 'use-healthchecks',\n                'model' => Healthcheck::class,\n            ],\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Http/Livewire/Sites.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Http\\Livewire;\n\nuse Illuminate\\View\\View;\nuse Livewire\\Component;\nuse Livewire\\WithPagination;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass Sites extends Component\n{\n    use WithPagination;\n\n    public function render(): View\n    {\n        $sites = Site::query()\n            ->with([\n                'uptimeMonitor.downtimes',\n                'lighthouseMonitors.lighthouseResults',\n                'crawler',\n                'certificateMonitor',\n            ])\n            ->paginate(10);\n\n        /** @var view-string $view */\n        $view = 'sites::livewire.sites';\n\n        return view($view, [\n            'sites' => $sites,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Http/Livewire/Tables/SiteTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Http\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Enumerable;\nuse RamonRietdijk\\LivewireTables\\Actions\\Action;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\Enums\\Status;\nuse Vigilant\\Frontend\\Integrations\\Table\\StatusColumn;\nuse Vigilant\\Lighthouse\\Livewire\\Tables\\LighthouseMonitorsTable;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Lighthouse\\Models\\LighthouseResult;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Uptime\\Actions\\CalculateUptimePercentage;\nuse Vigilant\\Uptime\\Models\\Downtime;\n\nclass SiteTable extends BaseTable\n{\n    protected string $model = Site::class;\n\n    /**\n     * @return array<int, \\RamonRietdijk\\LivewireTables\\Columns\\BaseColumn>\n     */\n    protected function columns(): array\n    {\n        /** @var CalculateUptimePercentage $calculateUptime */\n        $calculateUptime = app(CalculateUptimePercentage::class);\n\n        return [\n            Column::make(__('URL'), 'url'),\n\n            Column::make(__('Average Lighthouse Score'))\n                ->displayUsing(function (Site $site) {\n\n                    /** @var ?LighthouseMonitor $monitor */\n                    $monitor = $site->lighthouseMonitors()->first();\n\n                    if ($monitor === null) {\n                        return null;\n                    }\n\n                    /** @var ?LighthouseResult $result */\n                    $result = $monitor->lighthouseResults()->orderByDesc('id')->first();\n\n                    if ($result === null) {\n                        return __('No Results');\n                    }\n\n                    $scores = [\n                        $result->performance,\n                        $result->accessibility,\n                        $result->best_practices,\n                        $result->seo,\n                    ];\n\n                    $score = array_sum($scores) / count($scores);\n\n                    return LighthouseMonitorsTable::scoreDisplay($score);\n                })\n                ->asHtml(),\n\n            Column::make(__('Uptime'))\n                ->displayUsing(function (Site $site) use ($calculateUptime) {\n\n                    $monitor = $site->uptimeMonitor;\n\n                    if ($monitor === null) {\n                        return null;\n                    }\n\n                    $percentage = $calculateUptime->calculate($monitor);\n\n                    if ($percentage === null) {\n                        return __('Not available yet');\n                    }\n\n                    $class = match (true) {\n                        $percentage > 95 => 'text-green-light',\n                        $percentage > 80 => 'text-orange',\n                        default => 'text-red'\n                    };\n\n                    return \"<span class='$class'>$percentage%</span>\";\n                })\n                ->asHtml(),\n\n            Column::make(__('Last downtime'))\n                ->displayUsing(function (Site $site) {\n                    $monitor = $site->uptimeMonitor;\n\n                    if ($monitor === null) {\n                        return null;\n                    }\n\n                    /** @var ?Downtime $lastDowntime */\n                    $lastDowntime = $monitor->downtimes()\n                        ->whereNotNull('end')\n                        ->orderByDesc('start')\n                        ->first();\n\n                    if ($lastDowntime === null) {\n                        return __('Never');\n                    }\n\n                    return teamTimezone($lastDowntime->start)->diffForHumans();\n\n                }),\n\n            StatusColumn::make(__('Link Issues'))\n                ->text(function (Site $site) {\n                    $crawler = $site->crawler;\n                    if ($crawler === null) {\n                        return null;\n                    }\n\n                    return __(':count issues', ['count' => $crawler->issueCount() ?? '0']);\n                })\n                ->status(function (Site $site): ?Status {\n                    $crawler = $site->crawler;\n\n                    if ($crawler === null) {\n                        return null;\n                    }\n\n                    $count = $crawler->issueCount();\n\n                    if ($count === null || $count === 0) {\n                        return Status::Success;\n                    }\n\n                    $total = $crawler->totalUrlCount();\n\n                    $threshold = $total * 0.05;\n\n                    return $count > $threshold\n                        ? Status::Danger\n                        : Status::Warning;\n                }),\n\n            StatusColumn::make(__('Certificate'))\n                ->text(function (Site $site) {\n                    $certificate = $site->certificateMonitor;\n\n                    if ($certificate === null || $certificate->valid_to === null) {\n                        return null;\n                    }\n\n                    return __('Expires in :diff', [\n                        'diff' => teamTimezone($certificate->valid_to)->longAbsoluteDiffForHumans(),\n                    ]);\n                })\n                ->status(function (Site $site): ?Status {\n                    $certificate = $site->certificateMonitor;\n\n                    if ($certificate === null || $certificate->valid_to === null) {\n                        return null;\n                    }\n\n                    $validTo = $certificate->valid_to;\n                    $diff = now()->diffInDays($validTo);\n\n                    if ($diff > 30) {\n                        return Status::Success;\n                    }\n\n                    if ($diff > 7) {\n                        return Status::Warning;\n                    }\n\n                    return Status::Danger;\n                }),\n\n        ];\n    }\n\n    protected function actions(): array\n    {\n        return [\n            Action::make(__('Delete'), function (Enumerable $models): void {\n                $models->each(fn (Site $site) => $site->delete());\n            }, 'delete'),\n        ];\n    }\n\n    public function link(Model $model): ?string\n    {\n        return route('site.view', ['site' => $model]);\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Http/Livewire/Tabs/CertificateMonitor.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Http\\Livewire\\Tabs;\n\nuse Livewire\\Attributes\\Computed;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor as CertificateMonitorModel;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass CertificateMonitor extends Component\n{\n    #[Locked]\n    public int $siteId;\n\n    public bool $enabled = false;\n\n    public function mount(Site $site): void\n    {\n        $this->siteId = $site->id;\n        $this->enabled = $this->monitor()->exists;\n    }\n\n    #[Computed]\n    public function monitor(): CertificateMonitorModel\n    {\n        /** @var Site $site */\n        $site = Site::query()->findOrFail($this->siteId);\n\n        /** @var ?CertificateMonitorModel $monitor */\n        $monitor = $site->certificateMonitor;\n\n        if ($monitor === null) {\n            $monitor = new CertificateMonitorModel([\n                'site_id' => $site->id,\n                'domain' => preg_replace('/^https?:\\/\\//', '', $site->url),\n                'port' => 443,\n            ]);\n        }\n\n        return $monitor;\n    }\n\n    #[On('save')]\n    public function save(): void\n    {\n        if (! $this->enabled && $this->monitor()->exists) {\n            $this->monitor()->delete();\n        }\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'sites::livewire.tabs.certificate-monitor';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Http/Livewire/Tabs/Crawler.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Http\\Livewire\\Tabs;\n\nuse Livewire\\Attributes\\Computed;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\nuse Vigilant\\Crawler\\Models\\Crawler as CrawlerModel;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass Crawler extends Component\n{\n    #[Locked]\n    public int $siteId;\n\n    public bool $enabled = false;\n\n    public function mount(Site $site): void\n    {\n        $this->siteId = $site->id;\n        $this->enabled = $this->crawler()->exists;\n    }\n\n    #[Computed]\n    public function crawler(): CrawlerModel\n    {\n        /** @var Site $site */\n        $site = Site::query()->findOrFail($this->siteId);\n\n        /** @var ?CrawlerModel $crawler */\n        $crawler = $site->crawler;\n\n        if ($crawler === null) {\n            $crawler = new CrawlerModel([\n                'site_id' => $site->id,\n                'start_url' => $site->url,\n            ]);\n        }\n\n        return $crawler;\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'sites::livewire.tabs.crawler';\n\n        return view($view, [\n            'siteId' => $this->siteId,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Http/Livewire/Tabs/DnsMonitors.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Http\\Livewire\\Tabs;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass DnsMonitors extends Component\n{\n    #[Locked]\n    public int $siteId;\n\n    public function mount(Site $site): void\n    {\n        $this->siteId = $site->id;\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'sites::livewire.tabs.dns-monitors';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Http/Livewire/Tabs/HealthcheckMonitor.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Http\\Livewire\\Tabs;\n\nuse Livewire\\Attributes\\Computed;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\Healthchecks\\Enums\\Type;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass HealthcheckMonitor extends Component\n{\n    #[Locked]\n    public int $siteId;\n\n    public bool $enabled = false;\n\n    public function mount(Site $site): void\n    {\n        $this->siteId = $site->id;\n        $this->enabled = $this->healthcheck()->exists;\n    }\n\n    #[Computed]\n    public function healthcheck(): Healthcheck\n    {\n        /** @var Site $site */\n        $site = Site::query()->findOrFail($this->siteId);\n\n        /** @var ?Healthcheck $healthcheck */\n        $healthcheck = $site->healthcheck;\n\n        if ($healthcheck === null) {\n            $healthcheck = new Healthcheck([\n                'site_id' => $site->id,\n                'domain' => $site->url,\n                'type' => Type::Endpoint,\n                'enabled' => true,\n                'interval' => 60,\n            ]);\n        }\n\n        return $healthcheck;\n    }\n\n    #[On('save')]\n    public function save(): void\n    {\n        if (! $this->enabled && $this->healthcheck()->exists) {\n            $this->healthcheck()->delete();\n        }\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'sites::livewire.tabs.healthcheck-monitor';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Http/Livewire/Tabs/LighthouseMonitors.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Http\\Livewire\\Tabs;\n\nuse Illuminate\\Support\\Facades\\Gate;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\On;\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Component;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass LighthouseMonitors extends Component\n{\n    #[Locked]\n    public int $siteId;\n\n    public bool $enabled = false;\n\n    /** @var array<int, array<string, string|int>> $monitors */\n    #[Validate]\n    public array $monitors = [];\n\n    /** @return array<string, array<int, mixed>> */\n    public function rules(): array\n    {\n        return [\n            'monitors.*.id' => ['nullable'],\n            'monitors.*.url' => ['required', 'url'],\n            'monitors.*.interval' => ['required', 'in:'.implode(',', array_keys(config('lighthouse.intervals')))],\n        ];\n    }\n\n    /** @return array<string, string> */\n    public function validationAttributes(): array\n    {\n        return [\n            'monitors.*.url' => 'URL',\n        ];\n    }\n\n    public function mount(Site $site): void\n    {\n        $this->siteId = $site->id;\n\n        $this->monitors = $site->lighthouseMonitors->map(function (LighthouseMonitor $monitor): array {\n            return [\n                'id' => $monitor->id,\n                'url' => $monitor->url,\n                'interval' => $monitor->interval,\n            ];\n        })->toArray();\n    }\n\n    public function updated(): void\n    {\n        $this->validate();\n    }\n\n    #[On('save')]\n    public function save(): void\n    {\n        abort_if(Gate::denies('use-lighthouse'), 403);\n\n        $monitors = $this->validate()['monitors'] ?? [];\n\n        foreach ($monitors as $monitor) {\n            if (! blank($monitor['id'] ?? null)) {\n                /** @var LighthouseMonitor $model */\n                $model = LighthouseMonitor::query()->findOrFail($monitor['id']);\n            } else {\n                /** @var LighthouseMonitor $model */\n                $model = LighthouseMonitor::query()->newModelInstance([\n                    'site_id' => $this->siteId,\n                    'settings' => [],\n                ]);\n            }\n\n            $model->url = $monitor['url'];\n            $model->interval = $monitor['interval'];\n\n            $model->save();\n        }\n    }\n\n    public function addPage(): void\n    {\n        /** @var Site $site */\n        $site = Site::query()->findOrFail($this->siteId);\n\n        $defaultInterval = collect(config('lighthouse.intervals'))->keys()->first() ?? 60 * 24; // @phpstan-ignore-line\n\n        $this->monitors[] = [\n            'url' => $site->url,\n            'interval' => $defaultInterval,\n        ];\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'sites::livewire.tabs.lighthouse-monitor';\n\n        return view($view, [\n            'monitors' => $this->monitors,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Http/Livewire/Tabs/UptimeMonitor.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Http\\Livewire\\Tabs;\n\nuse Livewire\\Attributes\\Computed;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Uptime\\Enums\\Type;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass UptimeMonitor extends Component\n{\n    #[Locked]\n    public int $siteId;\n\n    public bool $enabled = false;\n\n    public function mount(Site $site): void\n    {\n        $this->siteId = $site->id;\n        $this->enabled = $this->monitor()->exists;\n    }\n\n    #[Computed]\n    public function monitor(): Monitor\n    {\n        /** @var Site $site */\n        $site = Site::query()->findOrFail($this->siteId);\n\n        /** @var ?Monitor $monitor */\n        $monitor = $site->uptimeMonitor;\n\n        if ($monitor === null) {\n            $monitor = new Monitor([\n                'site_id' => $site->id,\n                'name' => $site->url,\n                'type' => Type::Http,\n                'settings' => [\n                    'host' => $site->url,\n                ],\n            ]);\n        }\n\n        return $monitor;\n    }\n\n    #[On('save')]\n    public function save(): void\n    {\n        if (! $this->enabled && $this->monitor()->exists) {\n            $this->monitor()->delete();\n        }\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'sites::livewire.tabs.uptime-monitor';\n\n        return view($view);\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Jobs/ImportSiteJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Vigilant\\Sites\\Actions\\ImportSite;\n\nclass ImportSiteJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n\n    /** @param array<string, bool> $monitors */\n    public function __construct(\n        public int $teamId,\n        public string $domain,\n        public array $monitors,\n    ) {\n        $this->onQueue(config()->string('sites.queue'));\n    }\n\n    public function handle(ImportSite $importer): void\n    {\n        $importer->import(\n            teamId: $this->teamId,\n            domain: $this->domain,\n            monitors: $this->monitors,\n        );\n    }\n\n    public function uniqueId(): string\n    {\n        return $this->teamId.'-'.$this->domain;\n    }\n\n    /** @return array<int, string> */\n    public function tags(): array\n    {\n        return [\n            'team:'.$this->teamId,\n            $this->domain,\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Models/Site.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Certificates\\Models\\CertificateMonitor;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Crawler\\Models\\Crawler;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\nuse Vigilant\\Healthchecks\\Models\\Healthcheck;\nuse Vigilant\\Lighthouse\\Models\\LighthouseMonitor;\nuse Vigilant\\Sites\\Observers\\SiteObserver;\nuse Vigilant\\Uptime\\Models\\Monitor as UptimeMonitor;\n\n/**\n * @property int $id\n * @property int $team_id\n * @property string $url\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?UptimeMonitor $uptimeMonitor\n * @property ?Crawler $crawler\n * @property Collection<int, LighthouseMonitor> $lighthouseMonitors\n * @property Collection<int, DnsMonitor> $dnsMonitors\n * @property ?CertificateMonitor $certificateMonitor\n * @property ?Healthcheck $healthcheck\n */\n#[ObservedBy([SiteObserver::class])]\n#[ScopedBy([TeamScope::class])]\nclass Site extends Model\n{\n    protected $guarded = [];\n\n    public function uptimeMonitor(): HasOne\n    {\n        return $this->hasOne(UptimeMonitor::class);\n    }\n\n    public function lighthouseMonitors(): HasMany\n    {\n        return $this->hasMany(LighthouseMonitor::class);\n    }\n\n    public function dnsMonitors(): HasMany\n    {\n        return $this->hasMany(DnsMonitor::class);\n    }\n\n    public function crawler(): HasOne\n    {\n        return $this->hasOne(Crawler::class);\n    }\n\n    public function certificateMonitor(): HasOne\n    {\n        return $this->hasOne(CertificateMonitor::class);\n    }\n\n    public function healthcheck(): HasOne\n    {\n        return $this->hasOne(Healthcheck::class);\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/Observers/SiteObserver.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Observers;\n\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass SiteObserver\n{\n    public function creating(Site $site): void\n    {\n        $teamService = app(TeamService::class);\n\n        $site->team_id = $teamService->team()->id;\n    }\n}\n"
  },
  {
    "path": "packages/sites/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites;\n\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\nuse Livewire\\Livewire;\nuse Vigilant\\Core\\Facades\\Navigation;\nuse Vigilant\\Core\\Policies\\AllowAllPolicy;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Sites\\Conditions\\SiteCondition;\nuse Vigilant\\Sites\\Http\\Livewire\\ImportSites;\nuse Vigilant\\Sites\\Http\\Livewire\\SiteForm;\nuse Vigilant\\Sites\\Http\\Livewire\\Sites;\nuse Vigilant\\Sites\\Http\\Livewire\\Tables\\SiteTable;\nuse Vigilant\\Sites\\Http\\Livewire\\Tabs\\CertificateMonitor;\nuse Vigilant\\Sites\\Http\\Livewire\\Tabs\\Crawler;\nuse Vigilant\\Sites\\Http\\Livewire\\Tabs\\DnsMonitors;\nuse Vigilant\\Sites\\Http\\Livewire\\Tabs\\HealthcheckMonitor;\nuse Vigilant\\Sites\\Http\\Livewire\\Tabs\\LighthouseMonitors;\nuse Vigilant\\Sites\\Http\\Livewire\\Tabs\\UptimeMonitor;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Uptime\\Notifications\\DowntimeStartNotification;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function register(): void\n    {\n        $this\n            ->registerConfig()\n            ->registerActions();\n    }\n\n    protected function registerConfig(): static\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/sites.php', 'sites');\n\n        return $this;\n    }\n\n    protected function registerActions(): static\n    {\n\n        return $this;\n    }\n\n    public function boot(): void\n    {\n        $this\n            ->bootConfig()\n            ->bootMigrations()\n            ->bootCommands()\n            ->bootViews()\n            ->bootLivewire()\n            ->bootRoutes()\n            ->bootNavigation()\n            ->bootNotifications()\n            ->bootPolicies();\n    }\n\n    protected function bootConfig(): static\n    {\n        $this->publishes([\n            __DIR__.'/../config/sites.php' => config_path('sites.php'),\n        ], 'config');\n\n        return $this;\n    }\n\n    protected function bootMigrations(): static\n    {\n        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');\n\n        return $this;\n    }\n\n    protected function bootCommands(): static\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n\n            ]);\n        }\n\n        return $this;\n    }\n\n    protected function bootViews(): static\n    {\n        $this->loadViewsFrom(__DIR__.'/../resources/views', 'sites');\n\n        return $this;\n    }\n\n    protected function bootLivewire(): static\n    {\n        Livewire::component('sites', Sites::class);\n        Livewire::component('sites.create', SiteForm::class);\n        Livewire::component('sites.table', SiteTable::class);\n        Livewire::component('sites.import', ImportSites::class);\n\n        Livewire::component('sites.tabs.uptime-monitor', UptimeMonitor::class);\n        Livewire::component('sites.tabs.lighthouse-monitor', LighthouseMonitors::class);\n        Livewire::component('sites.tabs.dns-monitors', DnsMonitors::class);\n        Livewire::component('sites.tabs.crawler', Crawler::class);\n        Livewire::component('sites.tabs.certificate-monitor', CertificateMonitor::class);\n        Livewire::component('sites.tabs.healthcheck-monitor', HealthcheckMonitor::class);\n\n        return $this;\n    }\n\n    protected function bootRoutes(): static\n    {\n        if (! $this->app->routesAreCached()) {\n            Route::middleware(['web', 'auth'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/web.php'));\n        }\n\n        return $this;\n    }\n\n    protected function bootNavigation(): static\n    {\n        Navigation::path(__DIR__.'/../resources/navigation.php');\n\n        return $this;\n    }\n\n    protected function bootNotifications(): static\n    {\n        NotificationRegistry::registerCondition(DowntimeStartNotification::class, [\n            SiteCondition::class,\n        ]);\n\n        return $this;\n    }\n\n    protected function bootPolicies(): static\n    {\n        if (ce()) {\n            Gate::policy(Site::class, AllowAllPolicy::class);\n        }\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/sites/testbench.yaml",
    "content": "providers:\n  - Vigilant\\Sites\\ServiceProvider\n"
  },
  {
    "path": "packages/sites/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\Sites\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Livewire\\LivewireServiceProvider;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Sites\\ServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n            LivewireServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/uptime/composer.json",
    "content": "{\n    \"name\": \"vigilant/uptime\",\n    \"description\": \"Vigilant Uptime\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"geerlingguy/ping\": \"^1.2\",\n        \"guzzlehttp/guzzle\": \"^7.8\",\n        \"laravel/framework\": \"^12.0\",\n        \"livewire/livewire\": \"^3.4\",\n        \"vigilant/core\": \"@dev\",\n        \"vigilant/dns\": \"@dev\",\n        \"vigilant/frontend\": \"@dev\",\n        \"vigilant/notifications\": \"@dev\",\n        \"vigilant/sites\": \"@dev\",\n        \"vigilant/users\": \"@dev\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Uptime\\\\\": \"src\",\n            \"Vigilant\\\\Uptime\\\\Database\\\\Factories\\\\\": \"database/factories\",\n            \"Vigilant\\\\Users\\\\Database\\\\Factories\\\\\": \"../users/database/factories\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Uptime\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\Uptime\\\\ServiceProvider\"\n            ]\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/uptime/config/uptime.php",
    "content": "<?php\n\nreturn [\n    'queue' => 'uptime',\n\n    'intervals' => [\n        1 => 'Every second',\n        5 => 'Every 5 seconds',\n        10 => 'Every 10 seconds',\n        20 => 'Every 20 seconds',\n        30 => 'Every 30 seconds',\n        60 => 'Every minute',\n        300 => 'Every 5 minutes',\n        600 => 'Every 10 minutes',\n        3600 => 'Every hour',\n    ],\n\n    'allow_external_outposts' => env('UPTIME_ALLOW_EXTERNAL_OUTPOSTS', false),\n    'outpost_secret' => env('UPTIME_OUTPOST_SECRET', 'outpost-secret'),\n];\n"
  },
  {
    "path": "packages/uptime/database/factories/MonitorFactory.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Database\\Factories;\n\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Vigilant\\Uptime\\Enums\\Type;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass MonitorFactory extends Factory\n{\n    protected $model = Monitor::class;\n\n    public function definition(): array\n    {\n        return [\n            'team_id' => 1,\n            'name' => 'Monitor',\n            'type' => Type::Http,\n            'settings' => [\n                'host' => '1.1.1.1',\n            ],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/uptime/database/migrations/2024_02_21_190000_create_uptime_monitors_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Users\\Models\\Team;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('uptime_monitors', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(Site::class)->nullable()->constrained()->onDelete('cascade');\n            $table->foreignIdFor(Team::class)->constrained()->onDelete('cascade');\n\n            $table->string('name');\n            $table->string('type');\n\n            $table->json('settings');\n            $table->string('interval');\n\n            $table->integer('retries');\n            $table->integer('timeout');\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('uptime_monitors');\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2024_02_21_190100_create_uptime_results_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('uptime_results', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(Monitor::class)->index()->constrained('uptime_monitors')->onDelete('cascade');\n\n            $table->float('total_time');\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('uptime_results');\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2024_02_21_190200_create_uptime_downtimes_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('uptime_downtimes', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(Monitor::class)->index()->constrained('uptime_monitors')->onDelete('cascade');\n\n            $table->dateTime('start');\n            $table->dateTime('end')->nullable();\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('uptime_downtimes');\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2024_02_22_073000_create_uptime_results_aggregates_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('uptime_results_aggregates', function (Blueprint $table) {\n            $table->id();\n            $table->foreignIdFor(Monitor::class)->index()->constrained('uptime_monitors')->onDelete('cascade');\n\n            $table->float('total_time');\n\n            $table->timestamps();\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('uptime_results_aggregates');\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2024_05_21_170200_uptime_downtimes_data_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('uptime_downtimes', function (Blueprint $table) {\n            $table->json('data')->nullable()->after('end');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropColumns('uptime_downtimes', ['data']);\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2025_02_01_170000_uptime_monitors_enabled_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('uptime_monitors', function (Blueprint $table): void {\n            $table->boolean('enabled')->after('id')->default(true);\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropColumns('uptime_monitors', ['enabled']);\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2025_05_06_190000_uptime_monitors_schedule_fields.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::dropColumns('uptime_monitors', ['interval']);\n\n        Schema::table('uptime_monitors', function (Blueprint $table): void {\n            $table->timestamp('next_run')->nullable()->after('settings');\n            $table->integer('interval')->default(60)->after('next_run');\n            $table->string('state')->default('up')->after('name');\n            $table->integer('try')->default(0)->after('state');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('uptime_monitors', function (Blueprint $table): void {\n            $table->dropColumn(['next_run', 'interval', 'state']);\n        });\n\n        Schema::table('uptime_monitors', function (Blueprint $table): void {\n            $table->string('interval')->default('* * * * *')->after('settings');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2025_10_05_080000_create_uptime_outposts_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::create('uptime_outposts', function (Blueprint $table): void {\n            $table->id();\n\n            $table->string('ip');\n            $table->integer('port');\n            $table->string('external_ip');\n\n            $table->string('status')->index();\n\n            $table->string('country')->nullable();\n            $table->float('latitude', 10, 6)->nullable();\n            $table->float('longitude', 10, 6)->nullable();\n\n            $table->dateTime('last_available_at');\n            $table->timestamps();\n\n            $table->unique(['ip', 'port']);\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropIfExists('uptime_outposts');\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2025_10_06_181706_add_country_to_uptime_results_tables.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('uptime_results', function (Blueprint $table) {\n            $table->string('country')->nullable()->after('total_time');\n        });\n\n        Schema::table('uptime_results_aggregates', function (Blueprint $table) {\n            $table->string('country')->nullable()->after('total_time');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('uptime_results', function (Blueprint $table) {\n            $table->dropColumn('country');\n        });\n\n        Schema::table('uptime_results_aggregates', function (Blueprint $table) {\n            $table->dropColumn('country');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2025_10_06_183423_add_location_to_uptime_monitors_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('uptime_monitors', function (Blueprint $table) {\n            $table->float('latitude', 10, 6)->nullable()->after('timeout');\n            $table->float('longitude', 10, 6)->nullable()->after('latitude');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('uptime_monitors', function (Blueprint $table) {\n            $table->dropColumn(['latitude', 'longitude']);\n        });\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2025_10_06_184619_add_country_to_uptime_monitors_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('uptime_monitors', function (Blueprint $table) {\n            $table->string('country')->nullable()->after('timeout');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('uptime_monitors', function (Blueprint $table) {\n            $table->dropColumn('country');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2025_10_07_180857_add_geoip_fetched_at_to_uptime_monitors_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('uptime_monitors', function (Blueprint $table): void {\n            $table->timestamp('geoip_fetched_at')->nullable()->after('longitude');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('uptime_monitors', function (Blueprint $table): void {\n            $table->dropColumn('geoip_fetched_at');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2025_10_08_100000_add_closest_outpost_id_to_uptime_monitors_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Vigilant\\Uptime\\Models\\Outpost;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('uptime_monitors', function (Blueprint $table): void {\n            $table->foreignIdFor(Outpost::class, 'closest_outpost_id')->after('team_id')->nullable()->constrained('uptime_outposts')->onDelete('set null');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('uptime_monitors', function (Blueprint $table): void {\n            $table->dropForeign(['closest_outpost_id']);\n            $table->dropColumn('closest_outpost_id');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2025_10_19_152447_add_unavailable_at_to_uptime_outposts_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('uptime_outposts', function (Blueprint $table): void {\n            $table->dateTime('unavailable_at')->nullable()->after('last_available_at');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('uptime_outposts', function (Blueprint $table): void {\n            $table->dropColumn('unavailable_at');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2025_12_23_192903_add_geoip_automatic_to_uptime_monitors_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('uptime_monitors', function (Blueprint $table): void {\n            $table->boolean('geoip_automatic')->default(true)->after('geoip_fetched_at');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('uptime_monitors', function (Blueprint $table): void {\n            $table->dropColumn('geoip_automatic');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2025_12_23_193500_add_geoip_automatic_to_uptime_outposts_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('uptime_outposts', function (Blueprint $table): void {\n            $table->boolean('geoip_automatic')->default(true)->after('longitude');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::table('uptime_outposts', function (Blueprint $table): void {\n            $table->dropColumn('geoip_automatic');\n        });\n    }\n};\n"
  },
  {
    "path": "packages/uptime/database/migrations/2025_12_27_142900_update_ping_monitor_types.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Support\\Facades\\DB;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        DB::table('uptime_monitors')\n            ->where('type', '=', 'ping')\n            ->update(['type' => 'icmp']);\n    }\n\n    public function down(): void\n    {\n        DB::table('uptime_monitors')\n            ->where('type', '=', 'tcp')\n            ->update(['type' => 'ping']);\n    }\n};\n"
  },
  {
    "path": "packages/uptime/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n    ignoreErrors:\n        - identifier: missingType.iterableValue\n        - identifier: missingType.generics\n"
  },
  {
    "path": "packages/uptime/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/uptime/resources/navigation.php",
    "content": "<?php\n\nuse Vigilant\\Core\\Facades\\Navigation;\n\nNavigation::add(route('uptime'), 'Uptime')\n    ->icon('tni-double-caret-up-circle-o')\n    ->parent('health')\n    ->gate('use-uptime')\n    ->routeIs('uptime*')\n    ->sort(1);\n"
  },
  {
    "path": "packages/uptime/resources/views/components/empty-states/monitors.blade.php",
    "content": "<x-frontend::empty-state\n    :title=\"__('No Uptime Monitors Yet')\"\n    :description=\"__('Create your first uptime monitor to start tracking availability and response times.')\"\n    icon=\"phosphor-warning-circle\"\n    iconClass=\"h-12 w-12 text-green\"\n    iconWrapperClass=\"rounded-full bg-green/10 p-4 mb-6\"\n    :buttonHref=\"route('uptime.monitor.create')\"\n    :buttonText=\"__('Add Uptime Monitor')\"\n    buttonClass=\"bg-gradient-to-r from-green via-cyan to-green bg-300% hover:shadow-lg hover:shadow-green/30 transition-all duration-300\"\n/>\n"
  },
  {
    "path": "packages/uptime/resources/views/livewire/charts/column-latency-chart.blade.php",
    "content": "<div x-init=\"$wire.loadChart()\">\n    @if ($hasPoints)\n        <div x-data=\"{ show: false, loading: true }\">\n            <div style=\"height: {{ $height }}px;\" wire:ignore x-init=\"() => {\n                Livewire.on('{{ $identifier }}-update-chart', params => {\n                    config = params[0]\n\n                    show = config.data.labels.length > 0\n                    loading = false\n\n                    let chart = Chart.getChart('{{ $identifier }}');\n\n                    config.options.plugins.tooltip.callbacks = {\n                        label: function(context) {\n                            let unit = context.dataset.unit || '';\n\n                            return context.dataset.label + ': ' + context.formattedValue + ' ' + unit;\n                        }\n                    };\n\n                    if (typeof chart === 'undefined') {\n                        chart = new Chart(document.getElementById('{{ $identifier }}'), config);\n                    } else {\n                        chart.reset();\n                        chart.type = config.type;\n                        chart.data = config.data;\n                        chart.options = config.options;\n                        chart.update();\n                    }\n                });\n            }\">\n                <canvas wire:ignore id=\"{{ $identifier }}\"></canvas>\n            </div>\n        </div>\n    @else\n        <div class=\"flex items-center justify-center\">\n            <span class=\"text-xs text-base-200\">-</span>\n        </div>\n    @endif\n</div>\n"
  },
  {
    "path": "packages/uptime/resources/views/livewire/charts/latency-chart.blade.php",
    "content": "<div class=\"bg-base-950 py-4 px-2 rounded-md border border-base-800\">\n    <div class=\"flex justify-between items-start mb-3\">\n        @if ($availableCountries->count() > 1)\n            <div class=\"ml-3 space-y-2 flex-1\">\n                <div class=\"flex flex-wrap gap-2 items-center\">\n                    @foreach ($availableCountries as $country)\n                        <button wire:click=\"toggleCountry('{{ $country }}')\" wire:loading.attr=\"disabled\"\n                            wire:loading.class=\"opacity-50 cursor-not-allowed\" @class([\n                                'px-3 py-1 text-xs font-medium rounded-full transition-colors duration-200 cursor-pointer relative',\n                                'bg-blue text-white' => in_array($country, $selectedCountries),\n                                'bg-base-800 text-base-200 hover:bg-base-700' => !in_array(\n                                    $country,\n                                    $selectedCountries),\n                            ])>\n                            {{ strtoupper($country) }}\n                            @if ($closestCountry === $country)\n                                <span class=\"ml-1 text-xs opacity-75\" title=\"Closest outpost\">★</span>\n                            @endif\n                        </button>\n                    @endforeach\n                </div>\n            </div>\n        @else\n            <div class=\"flex-1\"></div>\n        @endif\n\n        <div class=\"mr-2 relative\" x-data=\"{ open: false }\">\n            <button @click=\"open = !open\" @click.away=\"open = false\"\n                class=\"px-3 py-1.5 text-xs font-medium rounded-md bg-base-800 text-base-200 hover:bg-base-700 transition-colors duration-200 flex items-center gap-2\"\n                wire:loading.attr=\"disabled\" wire:loading.class=\"opacity-50 cursor-not-allowed\">\n                <span>{{ $dateRangeOptions[$dateRange] }}</span>\n                <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n                </svg>\n            </button>\n\n            <div x-show=\"open\" x-transition:enter=\"transition ease-out duration-100\"\n                x-transition:enter-start=\"transform opacity-0 scale-95\"\n                x-transition:enter-end=\"transform opacity-100 scale-100\"\n                x-transition:leave=\"transition ease-in duration-75\"\n                x-transition:leave-start=\"transform opacity-100 scale-100\"\n                x-transition:leave-end=\"transform opacity-0 scale-95\"\n                class=\"absolute right-0 mt-2 w-40 rounded-md shadow-lg bg-base-900 border border-base-800 z-10\"\n                style=\"display: none;\">\n                <div class=\"py-1\">\n                    @foreach ($dateRangeOptions as $key => $label)\n                        <button wire:click=\"setDateRange('{{ $key }}')\" @click=\"open = false\"\n                            @class([\n                                'block w-full text-left px-4 py-2 text-sm transition-colors duration-200',\n                                'bg-blue text-white' => $dateRange === $key,\n                                'text-base-200 hover:bg-base-800' => $dateRange !== $key,\n                            ])>\n                            {{ $label }}\n                        </button>\n                    @endforeach\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div wire:init=\"loadChart\" x-data=\"{ show: false, loading: true }\">\n        <div style=\"height: {{ $height }}px;\" wire:ignore x-init=\"() => {\n            Livewire.on('{{ $identifier }}-update-chart', params => {\n                config = params[0]\n\n                show = config.data.labels.length > 0\n                loading = false\n\n                let chart = Chart.getChart('{{ $identifier }}');\n\n                config.options.plugins.tooltip.callbacks = {\n                    label: function(context) {\n                        let unit = context.dataset.unit || '';\n\n                        return context.dataset.label + ': ' + context.formattedValue + ' ' + unit;\n                    }\n                };\n\n                if (typeof chart === 'undefined') {\n                    chart = new Chart(document.getElementById('{{ $identifier }}'), config);\n                } else {\n                    chart.reset();\n                    chart.type = config.type;\n                    chart.data = config.data;\n                    chart.options = config.options;\n                    chart.update();\n                }\n            });\n        }\">\n            <canvas wire:ignore id=\"{{ $identifier }}\"></canvas>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "packages/uptime/resources/views/livewire/monitor/dashboard.blade.php",
    "content": "<div class=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n    <dl class=\"grid grid-cols-2 gap-4\">\n        <x-frontend::stats-card :title=\"$monitor->type->label()\">\n            {{ $monitor->settings['host'] ?? '' }}\n        </x-frontend::stats-card>\n\n        <x-frontend::stats-card :title=\"__('Last Downtime')\">\n            {{ $lastDowntime === null ? __('Never') : teamTimezone($lastDowntime->end)->diffForHumans() }}\n            @if ($lastDowntime !== null)\n                @lang('For :time', ['time' => teamTimezone($lastDowntime->start)->longAbsoluteDiffForHumans(teamTimezone($lastDowntime->end))])\n            @endif\n        </x-frontend::stats-card>\n\n        <x-frontend::stats-card :title=\"__('Uptime last 30 days')\">\n            @if ($uptime30d === null)\n                <x-frontend::mdash />\n            @else\n                {{ number_format($uptime30d, 2) }}%\n            @endif\n        </x-frontend::stats-card>\n\n        <x-frontend::stats-card :title=\"__('Uptime last 7 days')\">\n            @if ($uptime7d === null)\n                <x-frontend::mdash />\n            @else\n                {{ number_format($uptime7d, 2) }}%\n            @endif\n        </x-frontend::stats-card>\n    </dl>\n\n    <div class=\"md:col-span-2\">\n        <livewire:monitor-latency-chart :height=\"250\" :data=\"['monitorId' => $monitor->id]\" wire:key=\"latency-chart\" />\n    </div>\n</div>\n"
  },
  {
    "path": "packages/uptime/resources/views/livewire/monitor/form.blade.php",
    "content": "<div>\n    @if (!$inline)\n        <x-slot name=\"header\">\n            <x-page-header :title=\"$updating ? 'Edit Uptime Monitor - ' . $monitor->name : 'Add Uptime Monitor'\" :back=\"$updating ? route('uptime.monitor.view', ['monitor' => $monitor]) : route('uptime')\">\n            </x-page-header>\n        </x-slot>\n    @endif\n\n    <form wire:submit=\"save\">\n        <div class=\"max-w-7xl mx-auto\">\n            <x-card>\n                <div class=\"flex flex-col gap-4\">\n                    @if (!$inline)\n                        <x-form.checkbox field=\"form.enabled\" name=\"Enabled\"\n                            description=\"Enable or disable this monitor\" />\n                    @endif\n                    <x-form.text field=\"form.name\" name=\"Name\" description=\"Friendly name for this monitor\" />\n\n                    <div class=\"relative\">\n                        <x-form.select field=\"form.type\" name=\"Monitor Type\"\n                            description=\"Choose how this monitor should check if the service is up\">\n                            @foreach (\\Vigilant\\Uptime\\Enums\\Type::cases() as $type)\n                                <option value=\"{{ $type->value }}\">{{ $type->label() }}</option>\n                            @endforeach\n                        </x-form.select>\n\n                        <!-- Subtle inline loading indicator -->\n                        <div wire:loading wire:target=\"form.type\"\n                            class=\"absolute right-2 top-9 flex items-center gap-2 text-xs text-base-400\">\n                            <svg class=\"w-4 h-4 animate-spin text-blue\" fill=\"none\" viewBox=\"0 0 24 24\">\n                                <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\"\n                                    stroke-width=\"4\"></circle>\n                                <path class=\"opacity-75\" fill=\"currentColor\"\n                                    d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\">\n                                </path>\n                            </svg>\n                            <span>Loading...</span>\n                        </div>\n                    </div>\n\n                    <div wire:loading.class=\"opacity-50 pointer-events-none\" wire:target=\"form.type\"\n                        class=\"transition-opacity duration-200\">\n                        @if ($form->type === \\Vigilant\\Uptime\\Enums\\Type::Http->value)\n                            <x-form.text field=\"form.settings.host\" name=\"Host\" description=\"HTTP Host\"\n                                placeholder=\"{{ config('app.url') }}\" />\n                        @elseif ($form->type === \\Vigilant\\Uptime\\Enums\\Type::Ping->value)\n                            <x-form.text field=\"form.settings.host\" name=\"Host\"\n                                description=\"IP address or hostname to ping\" placeholder=\"1.1.1.1\" />\n                        @elseif ($form->type === \\Vigilant\\Uptime\\Enums\\Type::Tcp->value)\n                            <x-form.text field=\"form.settings.host\" name=\"Host\"\n                                description=\"Host or IP address of the service\"\n                                placeholder=\"{{ config('app.url') }} or 1.1.1.1\" />\n\n                            <x-form.number field=\"form.settings.port\" name=\"Port\" description=\"Port to check\" />\n                        @endif\n                    </div>\n\n                    <x-form.select field=\"form.interval\" name=\"Interval\"\n                        description=\"Choose how often this monitor should check the service\">\n                        @foreach (config('uptime.intervals') as $interval => $label)\n                            <option value=\"{{ $interval }}\">@lang($label)</option>\n                        @endforeach\n                    </x-form.select>\n\n                    <x-form.number field=\"form.retries\" name=\"Retries\"\n                        description=\"Amount of retries before marking the service as down\" />\n\n                    <x-form.number field=\"form.timeout\" name=\"Timeout\"\n                        description=\"Timeout for connecting to the service in seconds\" />\n\n                    <div class=\"border-t border-base-200 pt-4\">\n                        <div class=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n                            <div class=\"flex flex-col justify-center\">\n                                <p class=\"block text-base font-semibold leading-6 text-base-50\">@lang('Set location manually')</p>\n                                <span class=\"text-base-400 text-sm mt-1\">@lang('Override the location of this monitor manually.')</span>\n                            </div>\n                            <div class=\"flex flex-col justify-center\">\n                                <div class=\"flex items-center h-10\">\n                                    <button type=\"button\" role=\"switch\" x-data=\"{ automatic: $wire.entangle('form.geoip_automatic').live }\"\n                                        x-on:click=\"automatic = !automatic\" :aria-checked=\"(!automatic).toString()\"\n                                        :class=\"(!automatic) ? 'bg-gradient-to-r from-red to-orange' : 'bg-base-700'\"\n                                        class=\"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-red focus:ring-offset-2 focus:ring-offset-base-900\">\n                                        <span class=\"sr-only\">Set location manually</span>\n                                        <span :class=\"(!automatic) ? 'translate-x-5' : 'translate-x-0'\"\n                                            class=\"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-base-50 shadow-lg ring-0 transition duration-200 ease-in-out\"></span>\n                                    </button>\n                                </div>\n                                @error('form.geoip_automatic')\n                                    <span class=\"text-red text-sm mt-1\">{{ $message }}</span>\n                                @enderror\n                            </div>\n                        </div>\n\n                        @if (!$form->geoip_automatic)\n                            <div class=\"grid grid-cols-1 gap-4 md:grid-cols-3 mt-4\">\n                                <x-form.text field=\"form.country\" name=\"Country\"\n                                    description=\"Two-letter country code (e.g. US)\" />\n\n                                <x-form.number field=\"form.latitude\" name=\"Latitude\" description=\"Between -90 and 90\"\n                                    step=\"any\" min=\"-90\" max=\"90\" placeholder=\"43.6532\" />\n\n                                <x-form.number field=\"form.longitude\" name=\"Longitude\"\n                                    description=\"Between -180 and 180\" step=\"any\" min=\"-180\" max=\"180\"\n                                    placeholder=\"-79.3832\" />\n                            </div>\n                        @endif\n                    </div>\n\n                    @if (!$inline)\n                        <x-form.submit-button dusk=\"submit-button\" :submitText=\"$updating ? 'Save' : 'Create'\" />\n                    @endif\n                </div>\n            </x-card>\n        </div>\n    </form>\n</div>\n"
  },
  {
    "path": "packages/uptime/resources/views/livewire/uptime-monitors.blade.php",
    "content": "<div>\n    <x-slot name=\"header\">\n        <x-page-header title=\"Uptime Monitoring\">\n            <x-frontend::page-header.actions>\n                <x-create-button dusk=\"monitor-add-button\" :href=\"route('uptime.monitor.create')\" model=\"Vigilant\\Uptime\\Models\\Monitor\">\n                    @lang('Add Uptime Monitor')\n                </x-create-button>\n            </x-frontend::page-header.actions>\n            <x-frontend::page-header.mobile-actions>\n                <x-create-button-dropdown :href=\"route('uptime.monitor.create')\" model=\"Vigilant\\Uptime\\Models\\Monitor\">\n                    @lang('Add Uptime Monitor')\n                </x-create-button-dropdown>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    @if ($hasMonitors)\n        <livewire:uptime-monitor-table />\n    @else\n        <x-uptime::empty-states.monitors />\n    @endif\n</div>\n"
  },
  {
    "path": "packages/uptime/resources/views/monitor/view.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header :back=\"route('uptime')\" :title=\"'Uptime Monitor - ' . $monitor->name . (!$monitor->enabled ? ' (Disabled)' : '')\">\n            <x-frontend::page-header.actions>\n                <x-form.button dusk=\"monitor-edit-button\" :href=\"route('uptime.monitor.edit', ['monitor' => $monitor])\">\n                    @lang('Edit')\n                </x-form.button>\n                <x-form.button class=\"bg-red\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.button>\n            </x-frontend::page-header.actions>\n            \n            <x-frontend::page-header.mobile-actions>\n                <x-form.dropdown-button :href=\"route('uptime.monitor.edit', ['monitor' => $monitor])\">\n                    @lang('Edit')\n                </x-form.dropdown-button>\n                <x-form.dropdown-button class=\"!text-red hover:!text-red-light\" @click=\"$dispatch('open-delete-modal')\">\n                    @lang('Delete')\n                </x-form.dropdown-button>\n            </x-frontend::page-header.mobile-actions>\n        </x-page-header>\n    </x-slot>\n\n    <livewire:monitor-dashboard :monitorId=\"$monitor->id\" />\n\n    <div class=\"mt-4\">\n        <h2 class=\"text-xl font-bold leading-7 sm:truncate sm:text-2xl sm:tracking-tight text-neutral-100 mb-2\">\n            {{ __('Downtimes') }}\n        </h2>\n\n        <livewire:uptime-downtime-table :monitorId=\"$monitor->id\" wire:key=\"downtime-table\" />\n    </div>\n\n    <!-- Delete Confirmation Modal -->\n    <div x-data=\"{ showDeleteModal: false }\" @open-delete-modal.window=\"showDeleteModal = true\">\n        <x-frontend::modal show=\"showDeleteModal\">\n            <x-frontend::modal.header icon=\"phosphor-trash\" iconColor=\"red\" show=\"showDeleteModal\">\n                @lang('Delete Uptime Monitor')\n            </x-frontend::modal.header>\n\n            <x-frontend::modal.body>\n                <div class=\"space-y-4\">\n                    <p class=\"text-base-100\">\n                        @lang('Are you sure you want to delete this uptime monitor?')\n                    </p>\n                    <div class=\"bg-base-850 border border-base-700 rounded-lg p-4\">\n                        <div class=\"flex items-start gap-3\">\n                            <div class=\"flex-shrink-0\">\n                                @svg('phosphor-warning-circle', 'w-5 h-5 text-orange mt-0.5')\n                            </div>\n                            <div class=\"flex-1\">\n                                <p class=\"text-sm text-base-300\">\n                                    <span class=\"font-semibold text-base-100\">{{ $monitor->name }}</span>\n                                </p>\n                                <p class=\"text-sm text-base-400 mt-1\">\n                                    @lang('This action cannot be undone. All uptime history and downtime records for this monitor will be permanently deleted.')\n                                </p>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </x-frontend::modal.body>\n\n            <x-frontend::modal.footer>\n                <x-form.button type=\"button\" @click=\"showDeleteModal = false\">\n                    @lang('Cancel')\n                </x-form.button>\n                <form action=\"{{ route('uptime.monitor.delete', ['monitor' => $monitor]) }}\" method=\"POST\" class=\"inline\">\n                    @csrf\n                    @method('DELETE')\n                    <x-form.button class=\"bg-red\" type=\"submit\">\n                        @lang('Delete Monitor')\n                    </x-form.button>\n                </form>\n            </x-frontend::modal.footer>\n        </x-frontend::modal>\n    </div>\n\n</x-app-layout>\n"
  },
  {
    "path": "packages/uptime/routes/api.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\Uptime\\Http\\Controllers\\Api\\OutpostController;\nuse Vigilant\\Uptime\\Http\\Controllers\\Api\\OutpostIpController;\nuse Vigilant\\Uptime\\Http\\Middleware\\ExternalOutpostMiddleware;\nuse Vigilant\\Uptime\\Http\\Middleware\\OutpostAuthMiddleware;\n\nRoute::prefix('v1')->group(function (): void {\n    Route::prefix('outposts')\n        ->middleware([OutpostAuthMiddleware::class, ExternalOutpostMiddleware::class])\n        ->group(function (): void {\n            Route::post('register', [OutpostController::class, 'register']);\n            Route::post('unregister', [OutpostController::class, 'unregister']);\n        });\n\n    Route::get('uptime/ips/{format}', [OutpostIpController::class, 'list'])\n        ->middleware('throttle:uptime-ips');\n});\n"
  },
  {
    "path": "packages/uptime/routes/web.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\Uptime\\Http\\Controllers\\Api\\OutpostController;\nuse Vigilant\\Uptime\\Http\\Controllers\\UptimeMonitorController;\nuse Vigilant\\Uptime\\Http\\Livewire\\UptimeMonitorForm;\nuse Vigilant\\Uptime\\Http\\Livewire\\UptimeMonitors;\n\nRoute::prefix('uptime')\n    ->middleware('can:use-uptime')\n    ->group(function (): void {\n        Route::get('/', UptimeMonitors::class)->name('uptime');\n        Route::get('/create-monitor', UptimeMonitorForm::class)->name('uptime.monitor.create');\n        Route::get('/monitor/{monitor}', [UptimeMonitorController::class, 'index'])->name('uptime.monitor.view');\n        Route::delete('/monitor/{monitor}', [UptimeMonitorController::class, 'delete'])->name('uptime.monitor.delete')->can('delete,monitor');\n        Route::get('/monitor/{monitor}/edit', UptimeMonitorForm::class)->name('uptime.monitor.edit');\n\n        Route::get('outposts', [OutpostController::class, 'list'])\n            ->middleware('auth');\n    });\n"
  },
  {
    "path": "packages/uptime/src/Actions/AggregateResults.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Actions;\n\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Models\\Result;\nuse Vigilant\\Uptime\\Models\\ResultAggregate;\n\nclass AggregateResults\n{\n    public function aggregate(Monitor $monitor): void\n    {\n        $results = $monitor\n            ->results()\n            ->select(['id', 'country', 'created_at'])\n            ->where('created_at', '<', now()->subHour())\n            ->get();\n\n        $groupedByCountry = $results->groupBy('country');\n\n        foreach ($groupedByCountry as $country => $countryResults) {\n            /** @var \\Illuminate\\Support\\Collection<int, Result> $countryResults */\n            $groupedByHour = $countryResults->groupBy(function (Result $result) {\n                return $result->created_at?->startOfHour()->toDateTimeString() ?? '';\n            });\n\n            foreach ($groupedByHour as $hour => $hourResults) {\n                $ids = $hourResults->pluck('id')->toArray();\n\n                $averageTotalTime = Result::query()\n                    ->whereIn('id', $ids)\n                    ->average('total_time');\n\n                ResultAggregate::query()->create([\n                    'monitor_id' => $monitor->id,\n                    'total_time' => $averageTotalTime,\n                    'country' => $country,\n                    'created_at' => $hour,\n                    'updated_at' => $hour,\n                ]);\n\n                Result::query()->whereIn('id', $ids)->delete();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Actions/CalculateUptimePercentage.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Actions;\n\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Uptime\\Models\\Downtime;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Models\\ResultAggregate;\n\nclass CalculateUptimePercentage\n{\n    public function calculate(Monitor $monitor, string $carbonModifier = '-30 days'): ?float\n    {\n        $dateThreshold = Carbon::now()->modify($carbonModifier);\n\n        /** @var ?ResultAggregate $firstResult */\n        $firstResult = $monitor->aggregatedResults()\n            ->where('created_at', '>=', $dateThreshold)\n            ->orderBy('created_at')\n            ->first();\n\n        if ($firstResult === null || $firstResult->created_at === null) {\n            return null;\n        }\n\n        $minutesSinceFirstResult = $firstResult->created_at->diffInMinutes(now());\n\n        $downtimes = $monitor->downtimes()\n            ->where('created_at', '>=', $firstResult->created_at)\n            ->whereNotNull('end')\n            ->get();\n\n        $downtimeMinutes = 0;\n\n        /** @var Downtime $downtime */\n        foreach ($downtimes as $downtime) {\n\n            $duration = $downtime->start->diffInMinutes($downtime->end);\n\n            $downtimeMinutes += $duration;\n        }\n\n        $totalMinutes = $minutesSinceFirstResult - $downtimeMinutes;\n        $uptimePercentage = ($totalMinutes / $minutesSinceFirstResult) * 100;\n\n        return floor($uptimePercentage * 100) / 100;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Actions/CheckLatency.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Actions;\n\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Notifications\\LatencyChangedNotification;\nuse Vigilant\\Uptime\\Notifications\\LatencyPeakNotification;\n\nclass CheckLatency\n{\n    public function check(Monitor $monitor): void\n    {\n        $countries = $monitor->results()\n            ->distinct('country')\n            ->pluck('country')\n            ->filter()\n            ->all();\n\n        foreach ($countries as $country) {\n            $this->checkForCountry($monitor, $country);\n        }\n    }\n\n    protected function checkForCountry(Monitor $monitor, string $country): void\n    {\n        $currentAverage = (float) $monitor->results()\n            ->where('country', '=', $country)\n            ->average('total_time');\n\n        $averages = $monitor->aggregatedResults()\n            ->where('country', '=', $country)\n            ->orderByDesc('created_at')\n            ->take(12); // Past 12 hours\n\n        // Skip if we don't have enough data\n        if ($averages->count() < 10) {\n            return;\n        }\n\n        $aggregatedAverage = (float) $averages->average('total_time');\n\n        if ($currentAverage > 0 && $aggregatedAverage > 0) {\n            $percentageDifference = round((($currentAverage - $aggregatedAverage) / $aggregatedAverage) * 100);\n\n            if (abs($percentageDifference) > 0) {\n                LatencyChangedNotification::notify(\n                    $monitor,\n                    $percentageDifference,\n                    $aggregatedAverage,\n                    $currentAverage,\n                    $country\n                );\n            }\n\n            // Check for peak - recent results are significantly higher\n            $this->checkForPeak($monitor, $country, $aggregatedAverage);\n        }\n    }\n\n    protected function checkForPeak(Monitor $monitor, string $country, float $aggregatedAverage): void\n    {\n        // Get all recent results to calculate average\n        $allRecentResults = $monitor->results()\n            ->where('country', '=', $country)\n            ->orderByDesc('created_at')\n            ->pluck('total_time');\n\n        if ($allRecentResults->count() < 15) {\n            return;\n        }\n\n        // Get the last 5 checks\n        $lastFiveResults = $allRecentResults->take(5);\n\n        // Calculate average excluding the last 5 results to avoid skewing\n        $recentAverage = (float) $allRecentResults->slice(5)->average();\n\n        // Check if all of the last 5 checks are above the recent average\n        $allAboveAverage = $lastFiveResults->every(function ($latency) use ($recentAverage) {\n            return $latency > $recentAverage;\n        });\n\n        if ($allAboveAverage && $recentAverage > 0) {\n            $peakLatency = (float) $lastFiveResults->max();\n            $peakPercentIncrease = (($peakLatency - $recentAverage) / $recentAverage) * 100;\n\n            LatencyPeakNotification::notify(\n                $monitor,\n                $peakLatency,\n                $recentAverage,\n                $peakPercentIncrease,\n                $country\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Actions/CheckUptime.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Actions;\n\nuse Illuminate\\Http\\Client\\ConnectionException;\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Uptime\\Actions\\Outpost\\DetermineOutpost;\nuse Vigilant\\Uptime\\Actions\\Outpost\\GenerateRootCertificate;\nuse Vigilant\\Uptime\\Data\\UptimeResult;\nuse Vigilant\\Uptime\\Enums\\OutpostStatus;\nuse Vigilant\\Uptime\\Enums\\State;\nuse Vigilant\\Uptime\\Events\\DowntimeEndEvent;\nuse Vigilant\\Uptime\\Events\\DowntimeStartEvent;\nuse Vigilant\\Uptime\\Events\\UptimeCheckedEvent;\nuse Vigilant\\Uptime\\Models\\Downtime;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass CheckUptime\n{\n    public function __construct(\n        protected DetermineOutpost $determineOutpost,\n        protected GenerateRootCertificate $rootCertificateGenerator,\n    ) {}\n\n    public function check(Monitor $monitor): void\n    {\n        $monitor->update([\n            'next_run' => now()->addSeconds($monitor->interval),\n        ]);\n\n        $result = null;\n        $outpostCountry = null;\n        $excludedOutpostIds = [];\n        $maxAttempts = 3;\n\n        // Add 5s to give some buffer time for the outpost to respond\n        $timeout = $monitor->retries > 0\n            ? ($monitor->timeout * $monitor->retries) + 5\n            : $monitor->timeout + 5;\n\n        for ($i = 0; $i < $maxAttempts; $i++) {\n            $isLastAttempt = $i === $maxAttempts - 1;\n            $outpost = $this->determineOutpost->determine($monitor, $excludedOutpostIds);\n\n            if ($outpost === null) {\n                logger()->error('No outpost available for uptime check');\n\n                if ($isLastAttempt) {\n                    $result = new UptimeResult(\n                        up: false,\n                        totalTime: 0,\n                        country: null,\n                        data: ['error' => 'no_outpost_available'],\n                    );\n\n                    break;\n                }\n\n                continue;\n            }\n\n            $excludedOutpostIds[] = $outpost->id;\n\n            $certPath = $this->rootCertificateGenerator->getRootCertificatePath();\n\n            try {\n                $response = Http::baseUrl($outpost->url())\n                    ->withToken(config('uptime.outpost_secret'))\n                    ->withOptions([\n                        'verify' => $certPath,\n                    ])\n                    ->timeout($timeout)\n                    ->post('run-check', [\n                        'type' => $monitor->type->outpostValue(),\n                        'target' => $monitor->type->formatTarget($monitor),\n                        'timeout' => $monitor->timeout,\n                    ]);\n\n            } catch (ConnectionException $e) {\n                $message = strtolower($e->getMessage());\n                $isTimeout = str_contains($message, 'timed out');\n\n                if (! $isTimeout) {\n                    $outpost->update([\n                        'status' => OutpostStatus::Unavailable,\n                    ]);\n                }\n\n                logger()->error(\n                    $isTimeout\n                        ? 'Outpost timed out during uptime check'\n                        : 'Outpost connection error during uptime check',\n                    [\n                        'outpost_id' => $outpost->id,\n                        'uptime_monitor_id' => $monitor->id,\n                        'target' => $monitor->type->formatTarget($monitor),\n                        'error' => $e->getMessage(),\n                    ]\n                );\n\n                if ($isLastAttempt) {\n                    $result = new UptimeResult(\n                        up: false,\n                        totalTime: 0,\n                        country: $outpost->country,\n                        data: [\n                            'error' => $isTimeout ? 'timeout' : 'connection_exception',\n                            'message' => $e->getMessage(),\n                        ],\n                    );\n\n                    break;\n                }\n\n                continue;\n            }\n\n            if ($response->successful()) {\n                $result = new UptimeResult(\n                    up: $response->json('up', false),\n                    totalTime: $response->json('latency_ms', 0),\n                    country: $outpost->country,\n                    data: $response->json(),\n                );\n\n                $outpostCountry = $outpost->country;\n\n                $outpost->update([\n                    'last_available_at' => now(),\n                ]);\n\n                if (! $result->up) {\n                    continue;\n                }\n\n                break;\n            }\n\n            $outpost->update([\n                'status' => OutpostStatus::Unavailable,\n            ]);\n            logger()->error('Outpost returned unsuccessful response during uptime check', [\n                'outpost' => $outpost->ip,\n                'status' => $response->status(),\n                'body' => $response->body(),\n                'uptime_monitor_id' => $monitor->id,\n                'target' => $monitor->type->formatTarget($monitor),\n            ]);\n\n            if ($isLastAttempt) {\n                $result = new UptimeResult(\n                    up: false,\n                    totalTime: 0,\n                    country: $outpost->country,\n                    data: [\n                        'error' => 'unsuccessful_response',\n                        'status' => $response->status(),\n                    ],\n                );\n\n                break;\n            }\n        }\n\n        if ($result === null) {\n            logger()->error('All outposts failed to perform uptime check');\n\n            return;\n        }\n\n        /** @var ?Downtime $currentDowntime */\n        $currentDowntime = $monitor->downtimes()\n            ->whereNull('end')\n            ->first();\n\n        if (! $result->up) {\n\n            if ($currentDowntime === null) {\n\n                if ($monitor->try <= $monitor->retries) {\n                    $monitor->update([\n                        'try' => $monitor->try + 1,\n                        'state' => State::Retrying,\n                    ]);\n\n                    return;\n                }\n\n                $monitor->downtimes()->create([\n                    'start' => now(),\n                    'data' => $result->data,\n                ]);\n\n                $monitor->update([\n                    'state' => State::Down,\n                    'try' => 0,\n                ]);\n\n                DowntimeStartEvent::dispatch($monitor);\n            }\n\n        } else {\n            if ($currentDowntime !== null) {\n\n                $currentDowntime->update([\n                    'end' => now(),\n                ]);\n\n                $monitor->update([\n                    'state' => State::Up,\n                    'try' => 0,\n                ]);\n\n                DowntimeEndEvent::dispatch($currentDowntime);\n            }\n\n            $monitor->results()->create([\n                'total_time' => $result->totalTime,\n                'country' => $outpostCountry,\n            ]);\n        }\n\n        event(new UptimeCheckedEvent($monitor));\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Actions/FetchGeolocation.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Actions;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Dns\\Actions\\ResolveRecord;\nuse Vigilant\\Dns\\Enums\\Type;\nuse Vigilant\\Dns\\Models\\DnsMonitor;\n\nclass FetchGeolocation\n{\n    public function __construct(protected ResolveRecord $resolveRecord) {}\n\n    public function fetch(string $target): ?array\n    {\n        $host = $this->extractHost($target);\n\n        if ($host === null) {\n            return null;\n        }\n\n        // If host is a domain, resolve it to an IP address\n        if (! $this->isIpAddress($host)) {\n            $host = $this->resolveToIp($host);\n\n            if ($host === null) {\n                return null;\n            }\n\n            $host = str($host)->before(',')->toString();\n        }\n\n        try {\n            $response = Http::timeout(10)\n                ->get('https://free.freeipapi.com/api/json/'.$host);\n\n            if (! $response->successful()) {\n                return null;\n            }\n\n            $data = $response->json();\n\n            return [\n                'country' => $data['countryCode'] ?? null,\n                'latitude' => $data['latitude'] ?? null,\n                'longitude' => $data['longitude'] ?? null,\n            ];\n        } catch (\\Exception $e) {\n            logger()->warning('Failed to fetch geolocation for '.$host, [\n                'error' => $e->getMessage(),\n            ]);\n\n            return null;\n        }\n    }\n\n    protected function extractHost(string $target): ?string\n    {\n        // If it's a URL, parse the hostname\n        if (str_starts_with($target, 'http://') || str_starts_with($target, 'https://')) {\n            $parsed = parse_url($target);\n\n            return $parsed['host'] ?? null;\n        }\n\n        // If it's in format host:port, extract the host part\n        if (str_contains($target, ':')) {\n            $parts = explode(':', $target);\n\n            return $parts[0];\n        }\n\n        // Otherwise, assume it's already a hostname or IP\n        return $target;\n    }\n\n    protected function isIpAddress(string $host): bool\n    {\n        return filter_var($host, FILTER_VALIDATE_IP) !== false;\n    }\n\n    protected function resolveToIp(string $domain): ?string\n    {\n        try {\n            $dnsMonitor = DnsMonitor::where('record', $domain)\n                ->whereIn('type', [Type::A, Type::AAAA])\n                ->where('enabled', '=', true)\n                ->first();\n\n            if ($dnsMonitor && $dnsMonitor->value) {\n                return $dnsMonitor->value;\n            }\n        } catch (\\Exception $e) {\n            // DNS monitor table may not exist (e.g., in tests), continue with DNS resolution\n        }\n\n        try {\n            $ip = $this->resolveRecord->resolve(Type::A, $domain);\n\n            if ($ip !== null) {\n                return $ip;\n            }\n\n            return $this->resolveRecord->resolve(Type::AAAA, $domain);\n        } catch (\\Exception $e) {\n            logger()->warning('Failed to resolve domain to IP for '.$domain, [\n                'error' => $e->getMessage(),\n            ]);\n\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Actions/Outpost/DetermineOutpost.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Actions\\Outpost;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Vigilant\\Uptime\\Enums\\OutpostStatus;\nuse Vigilant\\Uptime\\Jobs\\UpdateMonitorLocationJob;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Models\\Outpost;\n\nclass DetermineOutpost\n{\n    public function determine(?Monitor $monitor = null, array $excludedOutposts = []): ?Outpost\n    {\n        // If no monitor or monitor has no location, use random selection\n        if ($monitor === null || $monitor->latitude === null || $monitor->longitude === null) {\n            if ($monitor !== null && $monitor->shouldFetchGeoip()) {\n                UpdateMonitorLocationJob::dispatch($monitor);\n            }\n\n            return Outpost::query()\n                ->where('status', '=', OutpostStatus::Available)\n                ->when(count($excludedOutposts) > 0, fn (Builder $query) => $query->whereNotIn('id', $excludedOutposts))\n                ->inRandomOrder()\n                ->first();\n        }\n\n        // Update closest outpost if not set (don't exclude from finding closest)\n        if ($monitor->closest_outpost_id === null) {\n            $this->updateClosestOutpost($monitor);\n        }\n\n        // 50% of the time, select the closest outpost\n        if (rand(0, 1) === 0) {\n            return $this->selectClosestOutpost($monitor, $excludedOutposts);\n        }\n\n        // 50% of the time, select a remote outpost\n        return $this->selectRemoteOutpost($monitor, $excludedOutposts);\n    }\n\n    protected function updateClosestOutpost(Monitor $monitor): void\n    {\n        $closestOutpost = $this->findClosestOutpost($monitor);\n\n        if ($closestOutpost !== null) {\n            $monitor->update([\n                'closest_outpost_id' => $closestOutpost->id,\n            ]);\n        }\n    }\n\n    protected function selectClosestOutpost(Monitor $monitor, array $excludedOutposts): ?Outpost\n    {\n        // Try to use the cached closest outpost if it's not excluded\n        if ($monitor->closest_outpost_id !== null && ! in_array($monitor->closest_outpost_id, $excludedOutposts)) {\n            $outpost = Outpost::query()\n                ->where('id', $monitor->closest_outpost_id)\n                ->where('status', '=', OutpostStatus::Available)\n                ->first();\n\n            if ($outpost !== null) {\n                return $outpost;\n            }\n        }\n\n        // Find the next closest outpost that's not excluded\n        return $this->findClosestOutpost($monitor, $excludedOutposts);\n    }\n\n    protected function selectRemoteOutpost(Monitor $monitor, array $excludedOutposts): ?Outpost\n    {\n        $excludedIds = $excludedOutposts;\n        if ($monitor->closest_outpost_id !== null) {\n            $excludedIds[] = $monitor->closest_outpost_id;\n        }\n\n        $outpost = Outpost::query()\n            ->where('status', '=', OutpostStatus::Available)\n            ->when(count($excludedIds) > 0, fn (Builder $query) => $query->whereNotIn('id', $excludedIds))\n            ->inRandomOrder()\n            ->first();\n\n        // If no remote outpost available, fallback to any available outpost\n        if ($outpost === null) {\n            $outpost = Outpost::query()\n                ->where('status', '=', OutpostStatus::Available)\n                ->when(count($excludedOutposts) > 0, fn (Builder $query) => $query->whereNotIn('id', $excludedOutposts))\n                ->inRandomOrder()\n                ->first();\n        }\n\n        return $outpost;\n    }\n\n    protected function findClosestOutpost(Monitor $monitor, array $excludedOutposts = []): ?Outpost\n    {\n        $earthRadius = 6371; // Earth's radius in kilometers\n\n        return Outpost::query()\n            ->where('status', '=', OutpostStatus::Available)\n            ->whereNotNull('latitude')\n            ->whereNotNull('longitude')\n            ->when(count($excludedOutposts) > 0, fn (Builder $query) => $query->whereNotIn('id', $excludedOutposts))\n            ->selectRaw(\n                'uptime_outposts.*, '.\n                '(? * 2 * ASIN(SQRT('.\n                    'POW(SIN(RADIANS((latitude - ?)) / 2), 2) + '.\n                    'COS(RADIANS(?)) * COS(RADIANS(latitude)) * '.\n                    'POW(SIN(RADIANS((longitude - ?)) / 2), 2)'.\n                '))) as distance',\n                [$earthRadius, $monitor->latitude, $monitor->latitude, $monitor->longitude]\n            )\n            ->orderBy('distance')\n            ->first();\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Actions/Outpost/GenerateOutpostCertificate.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Actions\\Outpost;\n\nclass GenerateOutpostCertificate\n{\n    public function __construct(\n        protected GenerateRootCertificate $rootCertificateGenerator,\n    ) {}\n\n    public function generate(string $commonName, string $outpostIp, int $validityDays = 30): array\n    {\n        $rootCert = $this->rootCertificateGenerator->getRootCertificate();\n        $rootKey = $this->rootCertificateGenerator->getRootPrivateKey();\n\n        $rootCertResource = openssl_x509_read($rootCert);\n        $rootKeyResource = openssl_pkey_get_private($rootKey);\n\n        if ($rootCertResource === false) {\n            throw new \\RuntimeException('Failed to read root certificate: '.openssl_error_string());\n        }\n\n        if ($rootKeyResource === false) {\n            throw new \\RuntimeException('Failed to read root private key: '.openssl_error_string());\n        }\n\n        // Generate new private key\n        $privateKey = openssl_pkey_new([\n            'private_key_bits' => 2048,\n            'private_key_type' => OPENSSL_KEYTYPE_RSA,\n        ]);\n\n        $dn = [\n            'countryName' => 'US',\n            'stateOrProvinceName' => 'State',\n            'localityName' => 'City',\n            'organizationName' => 'Vigilant',\n            'organizationalUnitName' => 'Outpost',\n            'commonName' => $commonName,\n        ];\n\n        // Detect if IP or DNS and build SAN accordingly\n        $sanEntry = filter_var($outpostIp, FILTER_VALIDATE_IP)\n            ? \"IP:{$outpostIp}\"\n            : \"DNS:{$outpostIp}\";\n\n        // Build a minimal OpenSSL config that defines all required sections\n        $tmpConfig = tempnam(sys_get_temp_dir(), 'openssl_');\n        $configData = <<<CONF\n[ req ]\ndefault_bits       = 2048\ndistinguished_name = req_distinguished_name\nreq_extensions     = v3_req\nprompt             = no\n\n[ req_distinguished_name ]\nCN = {$commonName}\n\n[ v3_req ]\nsubjectAltName = {$sanEntry}\nCONF;\n\n        file_put_contents($tmpConfig, $configData);\n\n        // Create CSR using the custom config (so SAN gets included)\n        $csr = openssl_csr_new($dn, $privateKey, [\n            'digest_alg' => 'sha256',\n            'config' => $tmpConfig,\n            'req_extensions' => 'v3_req',\n        ]);\n\n        if ($csr === false || $csr === true) {\n            throw new \\RuntimeException('Failed to generate CSR: '.openssl_error_string());\n        }\n\n        // Sign CSR with the root CA\n        $cert = openssl_csr_sign(\n            $csr,\n            $rootCertResource,\n            $rootKeyResource,\n            $validityDays,\n            [\n                'digest_alg' => 'sha256',\n                'config' => $tmpConfig,\n                'x509_extensions' => 'v3_req',\n            ]\n        );\n\n        if ($cert === false) {\n            throw new \\RuntimeException('Failed to sign certificate: '.openssl_error_string());\n        }\n\n        openssl_pkey_export($privateKey, $privateKeyPem);\n        openssl_x509_export($cert, $certPem);\n\n        @unlink($tmpConfig);\n\n        return [\n            'certificate' => $certPem,\n            'private_key' => $privateKeyPem,\n            'root_certificate' => $rootCert,\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Actions/Outpost/GenerateRootCertificate.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Actions\\Outpost;\n\nuse Illuminate\\Contracts\\Filesystem\\Filesystem;\nuse Illuminate\\Support\\Facades\\Storage;\n\nclass GenerateRootCertificate\n{\n    private const ROOT_CA_KEY_PATH = 'certificates/root-ca.key';\n\n    private const ROOT_CA_CERT_PATH = 'certificates/root-ca.crt';\n\n    public function generate(): void\n    {\n        if ($this->exists()) {\n            return;\n        }\n\n        $privateKey = openssl_pkey_new([\n            'private_key_bits' => 4096,\n            'private_key_type' => OPENSSL_KEYTYPE_RSA,\n        ]);\n\n        $dn = [\n            'countryName' => 'NL',\n            'stateOrProvinceName' => 'State',\n            'localityName' => 'City',\n            'organizationName' => 'Vigilant',\n            'organizationalUnitName' => 'Uptime Monitoring',\n            'commonName' => 'Vigilant Root CA',\n        ];\n\n        $csr = openssl_csr_new($dn, $privateKey, ['digest_alg' => 'sha256']);\n\n        if ($csr === false || $csr === true) {\n            throw new \\RuntimeException('Failed to generate CSR: '.openssl_error_string());\n        }\n\n        $cert = openssl_csr_sign($csr, null, $privateKey, 3650, ['digest_alg' => 'sha256']);\n\n        if ($cert === false) {\n            throw new \\RuntimeException('Failed to sign certificate: '.openssl_error_string());\n        }\n\n        openssl_pkey_export($privateKey, $privateKeyPem);\n        openssl_x509_export($cert, $certPem);\n\n        $this->disk()->put(self::ROOT_CA_KEY_PATH, $privateKeyPem);\n        $this->disk()->put(self::ROOT_CA_CERT_PATH, $certPem);\n\n        chmod($this->disk()->path(self::ROOT_CA_KEY_PATH), 0600);\n        chmod($this->disk()->path(self::ROOT_CA_CERT_PATH), 0644);\n    }\n\n    public function exists(): bool\n    {\n        return $this->disk()->exists(self::ROOT_CA_KEY_PATH) && $this->disk()->exists(self::ROOT_CA_CERT_PATH);\n    }\n\n    public function getRootCertificatePath(): string\n    {\n        if (! $this->exists()) {\n            $this->generate();\n        }\n\n        return $this->disk()->path(self::ROOT_CA_CERT_PATH);\n    }\n\n    public function getRootCertificate(): string\n    {\n        if (! $this->exists()) {\n            $this->generate();\n        }\n\n        $cert = $this->disk()->get(self::ROOT_CA_CERT_PATH);\n\n        if ($cert === null) {\n            throw new \\RuntimeException('Failed to read root certificate from disk');\n        }\n\n        return $cert;\n    }\n\n    public function getRootPrivateKey(): string\n    {\n        if (! $this->exists()) {\n            $this->generate();\n        }\n\n        $key = $this->disk()->get(self::ROOT_CA_KEY_PATH);\n\n        if ($key === null) {\n            throw new \\RuntimeException('Failed to read root private key from disk');\n        }\n\n        return $key;\n    }\n\n    protected function disk(): Filesystem\n    {\n        return Storage::disk('local');\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Actions/Outpost/RegisterOutpost.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Actions\\Outpost;\n\nuse Vigilant\\Uptime\\Actions\\FetchGeolocation;\nuse Vigilant\\Uptime\\Enums\\OutpostStatus;\nuse Vigilant\\Uptime\\Models\\Outpost;\n\nclass RegisterOutpost\n{\n    public function __construct(\n        protected FetchGeolocation $fetchGeolocation,\n    ) {}\n\n    public function register(\n        string $externalIp,\n        string $ip,\n        int $port,\n        bool $geoipAutomatic = true,\n        ?string $country = null,\n        ?float $latitude = null,\n        ?float $longitude = null,\n    ): Outpost {\n        if (! $geoipAutomatic) {\n            return Outpost::query()->updateOrCreate([\n                'ip' => $ip,\n                'port' => $port,\n            ], [\n                'external_ip' => $externalIp,\n                'status' => OutpostStatus::Available,\n                'country' => $country !== null ? strtoupper($country) : null,\n                'latitude' => $latitude !== null ? (float) $latitude : null,\n                'longitude' => $longitude !== null ? (float) $longitude : null,\n                'geoip_automatic' => false,\n                'last_available_at' => now(),\n            ]);\n        }\n\n        $existingOutpost = Outpost::query()\n            ->where('ip', '=', $ip)\n            ->where('port', '=', $port)\n            ->first();\n\n        if ($existingOutpost && $existingOutpost->country !== null) {\n            $existingOutpost->external_ip = $externalIp;\n            $existingOutpost->last_available_at = now();\n            $existingOutpost->status = OutpostStatus::Available;\n\n            $existingOutpost->save();\n\n            return $existingOutpost;\n        }\n\n        $geolocation = $this->fetchGeolocation->fetch($externalIp);\n\n        return Outpost::query()->updateOrCreate([\n            'ip' => $ip,\n            'port' => $port,\n        ], [\n            'external_ip' => $externalIp,\n            'status' => OutpostStatus::Available,\n            'country' => isset($geolocation['country']) ? strtoupper($geolocation['country']) : null,\n            'latitude' => $geolocation['latitude'] ?? null,\n            'longitude' => $geolocation['longitude'] ?? null,\n            'geoip_automatic' => true,\n            'last_available_at' => now(),\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Commands/AggregateResultsCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Uptime\\Jobs\\AggregateResultsJob;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass AggregateResultsCommand extends Command\n{\n    protected $signature = 'uptime:aggregate-results';\n\n    protected $description = 'Aggregate the results of the uptime checks';\n\n    public function handle(): int\n    {\n        $monitors = Monitor::query()\n            ->withoutGlobalScopes()\n            ->get();\n\n        foreach ($monitors as $monitor) {\n            AggregateResultsJob::dispatch($monitor);\n        }\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Commands/CheckLatencyCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Uptime\\Actions\\CheckLatency;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass CheckLatencyCommand extends Command\n{\n    protected $signature = 'uptime:check-latency {monitorId}';\n\n    protected $description = 'Check latency results';\n\n    public function handle(CheckLatency $checkLatency, TeamService $teamService): int\n    {\n        /** @var int $monitorId */\n        $monitorId = $this->argument('monitorId');\n\n        /** @var Monitor $monitor */\n        $monitor = Monitor::query()->withoutGlobalScopes()->findOrFail($monitorId);\n\n        $teamService->setTeamById($monitor->team_id);\n\n        $checkLatency->check($monitor);\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Commands/CheckUnavailableOutpostsCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Uptime\\Enums\\OutpostStatus;\nuse Vigilant\\Uptime\\Jobs\\CheckUnavailableOutpostJob;\nuse Vigilant\\Uptime\\Models\\Outpost;\n\nclass CheckUnavailableOutpostsCommand extends Command\n{\n    protected $signature = 'uptime:check-unavailable-outposts';\n\n    protected $description = 'Check unavailable outposts and remove them if still unreachable after 15 minutes';\n\n    public function handle(): int\n    {\n        $outposts = Outpost::query()\n            ->where('status', '=', OutpostStatus::Unavailable)\n            ->whereNotNull('unavailable_at')\n            ->where('unavailable_at', '<=', now()->subMinutes(15))\n            ->get();\n\n        foreach ($outposts as $outpost) {\n            CheckUnavailableOutpostJob::dispatch($outpost);\n        }\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Commands/CheckUptimeCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Uptime\\Jobs\\CheckUptimeJob;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass CheckUptimeCommand extends Command\n{\n    protected $signature = 'uptime:check {monitorId}';\n\n    protected $description = 'Check uptime for a monitor';\n\n    public function handle(): int\n    {\n        /** @var int $monitorId */\n        $monitorId = $this->argument('monitorId');\n\n        /** @var Monitor $monitor */\n        $monitor = Monitor::query()->withoutGlobalScopes()->findOrFail($monitorId);\n\n        CheckUptimeJob::dispatch($monitor);\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Commands/GenerateRootCaCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Vigilant\\Uptime\\Actions\\Outpost\\GenerateRootCertificate;\n\nclass GenerateRootCaCommand extends Command\n{\n    protected $signature = 'uptime:generate-root-ca';\n\n    protected $description = 'Generate root CA certificate for outpost HTTPS';\n\n    public function handle(GenerateRootCertificate $generator): int\n    {\n        if ($generator->exists()) {\n            $this->info('Root CA certificate already exists.');\n\n            return static::SUCCESS;\n        }\n\n        $this->info('Generating root CA certificate...');\n\n        $generator->generate();\n\n        $this->info('Root CA certificate generated successfully.');\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Commands/ScheduleUptimeChecksCommand.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Vigilant\\Uptime\\Jobs\\CheckUptimeJob;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass ScheduleUptimeChecksCommand extends Command\n{\n    protected $signature = 'uptime:schedule';\n\n    protected $description = 'Schedule Uptime Jobs';\n\n    public function handle(): int\n    {\n        Monitor::query()\n            ->withoutGlobalScopes()\n            ->where('enabled', '=', true)\n            ->where(function (Builder $builder): void {\n                $builder->where('next_run', '<=', now())\n                    ->orWhereNull('next_run');\n            })\n            ->get()\n            ->each(function (Monitor $monitor): void {\n                CheckUptimeJob::dispatch($monitor);\n            });\n\n        return static::SUCCESS;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Data/UptimeResult.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Data;\n\nclass UptimeResult\n{\n    public function __construct(\n        public bool $up = true,\n        public float $totalTime = 0,\n        public ?string $country = null,\n        public array $data = [],\n    ) {}\n}\n"
  },
  {
    "path": "packages/uptime/src/Enums/OutpostStatus.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Enums;\n\nenum OutpostStatus: string\n{\n    case Available = 'available';\n    case Unavailable = 'unavailable';\n}\n"
  },
  {
    "path": "packages/uptime/src/Enums/State.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Enums;\n\nenum State: string\n{\n    case Up = 'up';\n    case Retrying = 'retrying';\n    case Down = 'down';\n}\n"
  },
  {
    "path": "packages/uptime/src/Enums/Type.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Enums;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nenum Type: string\n{\n    case Http = 'http';\n    case Ping = 'icmp';\n    case Tcp = 'tcp';\n\n    public function label(): string\n    {\n        return match ($this) {\n            Type::Http => 'HTTP(s)',\n            Type::Ping => 'Ping',\n            Type::Tcp => 'TCP',\n        };\n    }\n\n    public function outpostValue(): string\n    {\n        return match ($this) {\n            Type::Http => 'http',\n            Type::Ping => 'icmp',\n            Type::Tcp => 'tcp',\n        };\n    }\n\n    public function formatTarget(Monitor $monitor): string\n    {\n        if ($this === Type::Http) {\n            $validator = Validator::make($monitor->settings, [\n                'host' => ['required', 'url'],\n            ]);\n\n            if ($validator->fails()) {\n                $validator = Validator::make($monitor->settings, [\n                    'host' => ['required', 'ip'],\n                ]);\n            }\n\n            $settings = $validator->validate();\n\n            return $settings['host'];\n        }\n\n        if ($this === Type::Ping) {\n            $settings = Validator::validate($monitor->settings, [\n                'host' => ['required', 'ip'],\n            ]);\n\n            return $settings['host'];\n        }\n\n        $settings = Validator::validate($monitor->settings, [\n            'host' => ['required', 'ip'],\n            'port' => ['integer', 'min:1', 'max:65535'],\n        ]);\n\n        return sprintf('%s:%s', $settings['host'], $settings['port']);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Events/DowntimeEndEvent.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Events;\n\nuse Illuminate\\Foundation\\Events\\Dispatchable;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Uptime\\Models\\Downtime;\n\nclass DowntimeEndEvent\n{\n    use Dispatchable;\n    use SerializesModels;\n\n    public function __construct(\n        public Downtime $downtime\n    ) {}\n}\n"
  },
  {
    "path": "packages/uptime/src/Events/DowntimeStartEvent.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Events;\n\nuse Illuminate\\Foundation\\Events\\Dispatchable;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass DowntimeStartEvent\n{\n    use Dispatchable;\n    use SerializesModels;\n\n    public function __construct(\n        public Monitor $monitor\n    ) {}\n}\n"
  },
  {
    "path": "packages/uptime/src/Events/UptimeCheckedEvent.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Events;\n\nuse Illuminate\\Foundation\\Events\\Dispatchable;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass UptimeCheckedEvent\n{\n    use Dispatchable;\n    use SerializesModels;\n\n    public function __construct(\n        public Monitor $monitor\n    ) {}\n}\n"
  },
  {
    "path": "packages/uptime/src/Http/Controllers/Api/OutpostController.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Http\\Controllers\\Api;\n\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Validation\\Rule;\nuse Vigilant\\Frontend\\Validation\\CountryCode;\nuse Vigilant\\Uptime\\Actions\\Outpost\\GenerateOutpostCertificate;\nuse Vigilant\\Uptime\\Actions\\Outpost\\RegisterOutpost;\nuse Vigilant\\Uptime\\Models\\Outpost;\n\nclass OutpostController extends Controller\n{\n    public function register(\n        Request $request,\n        RegisterOutpost $registrar,\n        GenerateOutpostCertificate $certificateGenerator\n    ): JsonResponse {\n        $geoipAutomatic = ! ($request->filled('country') && $request->filled('latitude') && $request->filled('longitude'));\n\n        $request->validate([\n            'ip' => 'required|ip',\n            'port' => 'required|integer|min:1|max:65535',\n            'country' => [\n                'nullable',\n                'string',\n                new CountryCode,\n                Rule::requiredIf(! $geoipAutomatic),\n            ],\n            'latitude' => [\n                'nullable',\n                'numeric',\n                'between:-90,90',\n                Rule::requiredIf(! $geoipAutomatic),\n            ],\n            'longitude' => [\n                'nullable',\n                'numeric',\n                'between:-180,180',\n                Rule::requiredIf(! $geoipAutomatic),\n            ],\n        ]);\n\n        $clientIp = $request->ip();\n        if ($clientIp === null) {\n            return response()->json(['message' => 'Unable to determine client IP address.'], 400);\n        }\n\n        $outpost = $registrar->register(\n            $request->input('ip'),\n            $clientIp,\n            $request->input('port'),\n            $geoipAutomatic,\n            $request->input('country'),\n            $request->input('latitude'),\n            $request->input('longitude')\n        );\n\n        // Generate a short-lived certificate for the outpost (valid for 30 days)\n        $commonName = sprintf('outpost-%s-%d', $outpost->ip, $outpost->port);\n        $certificate = $certificateGenerator->generate($commonName, $clientIp, 30);\n\n        return response()->json($certificate);\n    }\n\n    public function unregister(Request $request): JsonResponse\n    {\n        $request->validate([\n            'ip' => 'required|ip',\n            'port' => 'required|integer|min:1|max:65535',\n        ]);\n\n        Outpost::query()\n            ->where('ip', $request->ip())\n            ->where('external_ip', $request->input('ip'))\n            ->where('port', $request->input('port'))\n            ->delete();\n\n        return response()->json(['message' => 'Outpost unregistered successfully.']);\n    }\n\n    public function list(): JsonResponse\n    {\n        $outposts = Outpost::query()->get();\n\n        return response()->json($outposts);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Http/Controllers/Api/OutpostIpController.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Http\\Controllers\\Api;\n\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Uptime\\Enums\\OutpostStatus;\nuse Vigilant\\Uptime\\Models\\Outpost;\n\nclass OutpostIpController extends Controller\n{\n    public function list(string $format): Response|JsonResponse\n    {\n        $ips = cache()->remember(\n            'uptime:outposts:ips',\n            now()->addMinutes(15),\n            fn (): Collection => Outpost::query()\n                ->select('external_ip')\n                ->distinct()\n                ->where('status', '=', OutpostStatus::Available)\n                ->pluck('external_ip')\n        );\n\n        if ($format === 'text') {\n            return response(\n                $ips->implode(\"\\n\"),\n                200,\n                ['Content-Type' => 'text/plain']\n            );\n        }\n\n        if ($format === 'json') {\n            return response()->json($ips);\n        }\n\n        return response()->json(['message' => 'Invalid format. Use \"text\" or \"json\".'], 400);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Http/Controllers/UptimeMonitorController.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Http\\Controllers;\n\nuse Illuminate\\Routing\\Controller;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass UptimeMonitorController extends Controller\n{\n    use DisplaysAlerts;\n\n    public function index(Monitor $monitor): mixed\n    {\n        /** @var view-string $view */\n        $view = 'uptime::monitor.view';\n\n        return view($view, [\n            'monitor' => $monitor,\n        ]);\n    }\n\n    public function delete(Monitor $monitor): mixed\n    {\n        $monitor->delete();\n\n        $this->alert(\n            __('Deleted'),\n            __('Uptime monitor was successfully deleted'),\n            AlertType::Success\n        );\n\n        return response()->redirectToRoute('uptime');\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Http/Livewire/Charts/ColumnLatencyChart.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Http\\Livewire\\Charts;\n\nuse Illuminate\\View\\View;\n\nclass ColumnLatencyChart extends LatencyChart\n{\n    public int $height = 40;\n\n    public function mount(array $data): void\n    {\n        parent::mount($data);\n\n        // Force date range to week for column chart\n        $this->dateRange = 'week';\n\n        // Ensure we have the closest country selected\n        $closestCountry = $this->getClosestCountry();\n        if ($closestCountry) {\n            $countries = $this->availableCountries();\n            if ($countries->contains($closestCountry)) {\n                $this->selectedCountries = [$closestCountry];\n            }\n        }\n    }\n\n    public function data(): array\n    {\n        $points = $this->points()->pluck('total_time')->map(fn (float $time): int => (int) round($time));\n\n        return [\n            'type' => 'line',\n            'data' => [\n                'labels' => $points,\n                'datasets' => [\n                    [\n                        'label' => 'Latency',\n                        'data' => $points,\n                        'pointRadius' => 0,\n                        'pointHoverRadius' => 0,\n                        'borderWidth' => 2,\n                        'borderColor' => '#337F1F',\n                        'tension' => 0.4,\n                    ],\n                ],\n            ],\n            'options' => [\n                'plugins' => [\n                    'legend' => [\n                        'display' => false,\n                    ],\n                    'tooltip' => [\n                        'enabled' => false,\n                    ],\n                ],\n                'scales' => [\n                    'y' => [\n                        'display' => false,\n                        'beginAtZero' => true,\n                    ],\n                    'x' => [\n                        'display' => false,\n                    ],\n                ],\n            ],\n        ];\n    }\n\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'uptime::livewire.charts.column-latency-chart';\n\n        return view($view, [\n            'identifier' => $this->getIdentifier(),\n            'height' => $this->height,\n            'hasPoints' => $this->points()->isNotEmpty(),\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Http/Livewire/Charts/LatencyChart.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Http\\Livewire\\Charts;\n\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Illuminate\\Support\\Str;\nuse Illuminate\\View\\View;\nuse Livewire\\Attributes\\Locked;\nuse Vigilant\\Frontend\\Http\\Livewire\\BaseChart;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Models\\Result;\nuse Vigilant\\Uptime\\Models\\ResultAggregate;\n\nclass LatencyChart extends BaseChart\n{\n    #[Locked]\n    public int $monitorId = 0;\n\n    public int $height = 200;\n\n    public array $selectedCountries = [];\n\n    public string $dateRange = 'week';\n\n    public function mount(array $data): void\n    {\n        Validator::make($data, [\n            'monitorId' => 'required',\n        ])->validate();\n\n        $this->monitorId = $data['monitorId'];\n\n        $countries = $this->availableCountries();\n        if ($countries->isNotEmpty()) {\n            $closestCountry = $this->getClosestCountry();\n\n            if ($closestCountry && $countries->contains($closestCountry)) {\n                $this->selectedCountries = [$closestCountry];\n            } else {\n                $this->selectedCountries = [$countries->first()];\n            }\n        }\n    }\n\n    public function toggleCountry(string $country): void\n    {\n        if (in_array($country, $this->selectedCountries)) {\n            $this->selectedCountries = array_values(array_diff($this->selectedCountries, [$country]));\n        } else {\n            $this->selectedCountries[] = $country;\n        }\n\n        $this->loadChart();\n    }\n\n    public function setDateRange(string $range): void\n    {\n        $this->dateRange = $range;\n        $this->loadChart();\n    }\n\n    public function selectAllCountries(): void\n    {\n        $this->selectedCountries = $this->availableCountries()->toArray();\n        $this->loadChart();\n    }\n\n    public function clearCountries(): void\n    {\n        $this->selectedCountries = [];\n        $this->loadChart();\n    }\n\n    protected function getClosestCountry(): ?string\n    {\n        $monitor = Monitor::query()\n            ->with('closestOutpost')\n            ->find($this->monitorId);\n\n        return $monitor?->closestOutpost?->country;\n    }\n\n    protected function getDateRangeStart(): Carbon\n    {\n        return match ($this->dateRange) {\n            'hour' => now()->subHour(),\n            'week' => now()->subWeek(),\n            'month' => now()->subMonth(),\n            '3months' => now()->subMonths(3),\n            '6months' => now()->subMonths(6),\n            default => now()->subWeek(),\n        };\n    }\n\n    protected function getDateRangeOptions(): array\n    {\n        return [\n            'hour' => 'Hour',\n            'week' => 'Week',\n            'month' => 'Month',\n            '3months' => '3 Months',\n            '6months' => '6 Months',\n        ];\n    }\n\n    protected function availableCountries(): Collection\n    {\n        // For hour range, use Result table; for others, use ResultAggregate\n        $model = $this->dateRange === 'hour' ? Result::class : ResultAggregate::class;\n\n        return $model::query()\n            ->where('monitor_id', '=', $this->monitorId)\n            ->whereNotNull('country')\n            ->where('country', '!=', '')\n            ->selectRaw('country, COUNT(*) as count')\n            ->groupBy('country')\n            ->orderByDesc('count')\n            ->get()\n            ->pluck('country');\n    }\n\n    protected function points(): Collection\n    {\n        // For hour range, use Result table; for others, use ResultAggregate\n        $model = $this->dateRange === 'hour' ? Result::class : ResultAggregate::class;\n\n        $query = $model::query()\n            ->where('monitor_id', '=', $this->monitorId)\n            ->where('created_at', '>=', $this->getDateRangeStart());\n\n        if (! empty($this->selectedCountries)) {\n            $query->whereIn('country', $this->selectedCountries);\n        }\n\n        return $query\n            ->orderBy('created_at', 'asc')\n            ->get();\n    }\n\n    public function data(): array\n    {\n        if (count($this->selectedCountries) > 1) {\n            return $this->multiCountryData();\n        }\n\n        return $this->singleLineData();\n    }\n\n    protected function singleLineData(): array\n    {\n        $points = $this->points();\n\n        $labels = $points->pluck('created_at');\n        $data = $points->pluck('total_time')->map(fn (float $time): int => (int) round($time));\n\n        $dateFormat = $this->dateRange === 'week' ? 'd/m H:i' : 'd/m';\n\n        if ($this->dateRange === 'hour') {\n            $dateFormat = 'H:i';\n        }\n\n        return [\n            'type' => 'line',\n            'data' => [\n                'labels' => $labels->map(fn (Carbon $carbon): string => teamTimezone($carbon)->format($dateFormat))->toArray(),\n                'datasets' => [\n                    [\n                        'label' => 'Latency',\n                        'data' => $data->toArray(),\n                        'pointRadius' => 0,\n                        'pointHoverRadius' => 0,\n                        'borderWidth' => 2,\n                        'borderColor' => '#337F1F',\n                        'tension' => 0.4,\n                        'unit' => 'ms',\n                    ],\n                ],\n            ],\n            'options' => [\n                'plugins' => [\n                    'legend' => [\n                        'display' => true,\n                    ],\n                    'tooltip' => [\n                        'enabled' => true,\n                    ],\n                ],\n                'scales' => [\n                    'y' => [\n                        'display' => true,\n                        'beginAtZero' => true,\n                    ],\n                    'x' => [\n                        'display' => true,\n                    ],\n                ],\n            ],\n        ];\n    }\n\n    protected function multiCountryData(): array\n    {\n        // Design system colors from styleguide\n        $colors = [\n            ['border' => '#3B82F6', 'bg' => 'rgba(59, 130, 246, 0.1)'],   // blue\n            ['border' => '#6366F1', 'bg' => 'rgba(99, 102, 241, 0.1)'],   // indigo\n            ['border' => '#10B981', 'bg' => 'rgba(16, 185, 129, 0.1)'],   // green\n            ['border' => '#F97316', 'bg' => 'rgba(249, 115, 22, 0.1)'],   // orange\n            ['border' => '#8B5CF6', 'bg' => 'rgba(139, 92, 246, 0.1)'],   // purple\n            ['border' => '#EC4899', 'bg' => 'rgba(236, 72, 153, 0.1)'],   // magenta\n            ['border' => '#06B6D4', 'bg' => 'rgba(6, 182, 212, 0.1)'],    // cyan\n            ['border' => '#EF4444', 'bg' => 'rgba(239, 68, 68, 0.1)'],    // red\n        ];\n\n        $limit = match ($this->dateRange) {\n            'hour' => 60,       // ~1 hour of minute data\n            'week' => 168,      // ~1 week of hourly data\n            'month' => 720,     // ~1 month of hourly data\n            '3months' => 2160,  // ~3 months of hourly data\n            '6months' => 4320,  // ~6 months of hourly data\n            default => 168,\n        };\n\n        $model = $this->dateRange === 'hour' ? Result::class : ResultAggregate::class;\n\n        $countryData = [];\n        $allTimestamps = collect();\n\n        foreach ($this->selectedCountries as $country) {\n            $query = $model::query()\n                ->where('monitor_id', '=', $this->monitorId)\n                ->where('country', '=', $country)\n                ->where('created_at', '>=', $this->getDateRangeStart());\n\n            $points = $query\n                ->orderBy('created_at', 'asc')\n                ->limit($limit)\n                ->get();\n\n            $countryData[$country] = [];\n            foreach ($points as $point) {\n                if ($point->created_at === null) {\n                    continue;\n                }\n                $timestamp = $point->created_at->timestamp;\n                $countryData[$country][$timestamp] = round($point->total_time); // @phpstan-ignore-line\n                $allTimestamps->push($timestamp);\n            }\n        }\n\n        $uniqueTimestamps = $allTimestamps->unique()->sort()->values();\n\n        $datasets = [];\n        foreach ($this->selectedCountries as $index => $country) {\n            $data = [];\n\n            foreach ($uniqueTimestamps as $timestamp) {\n                $data[] = $countryData[$country][$timestamp] ?? null;\n            }\n\n            $colorSet = $colors[$index % count($colors)];\n\n            $datasets[] = [\n                'label' => strtoupper($country),\n                'data' => $data,\n                'pointRadius' => 1,\n                'pointHoverRadius' => 4,\n                'borderWidth' => 2,\n                'borderColor' => $colorSet['border'],\n                'backgroundColor' => $colorSet['bg'],\n                'fill' => true,\n                'tension' => 0.4,\n                'unit' => 'ms',\n                'spanGaps' => true,\n            ];\n        }\n\n        $labels = $uniqueTimestamps->map(function ($timestamp) {\n            $dateFormat = $this->dateRange === 'hour' ? 'H:i' : 'd/m H:i';\n\n            return teamTimezone(Carbon::createFromTimestamp($timestamp))->format($dateFormat);\n        })->toArray();\n\n        return [\n            'type' => 'line',\n            'data' => [\n                'labels' => $labels,\n                'datasets' => $datasets,\n            ],\n            'options' => [\n                'responsive' => true,\n                'maintainAspectRatio' => false,\n                'plugins' => [\n                    'legend' => [\n                        'display' => true,\n                        'position' => 'top',\n                        'align' => 'end',\n                        'labels' => [\n                            'color' => '#D8D8E8', // base-200\n                            'font' => [\n                                'size' => 12,\n                            ],\n                            'padding' => 12,\n                            'usePointStyle' => true,\n                            'pointStyle' => 'circle',\n                        ],\n                    ],\n                    'tooltip' => [\n                        'enabled' => true,\n                        'mode' => 'index',\n                        'intersect' => false,\n                        'backgroundColor' => '#232333', // base-850\n                        'titleColor' => '#F4F4FA', // base-100\n                        'bodyColor' => '#D8D8E8', // base-200\n                        'borderColor' => '#444459', // base-700\n                        'borderWidth' => 1,\n                        'padding' => 12,\n                    ],\n                ],\n                'scales' => [\n                    'y' => [\n                        'display' => true,\n                        'beginAtZero' => true,\n                        'grid' => [\n                            'color' => '#2D2D42', // base-800\n                            'drawBorder' => false,\n                        ],\n                        'ticks' => [\n                            'color' => '#A8A8C0', // base-400\n                            'font' => [\n                                'size' => 11,\n                            ],\n                        ],\n                    ],\n                    'x' => [\n                        'display' => true,\n                        'grid' => [\n                            'display' => false,\n                        ],\n                        'ticks' => [\n                            'color' => '#A8A8C0', // base-400\n                            'font' => [\n                                'size' => 11,\n                            ],\n                            'maxRotation' => 0,\n                        ],\n                    ],\n                ],\n                'interaction' => [\n                    'mode' => 'index',\n                    'intersect' => false,\n                ],\n            ],\n        ];\n    }\n\n    protected function getIdentifier(): string\n    {\n        return Str::slug(get_class($this)).$this->monitorId;\n    }\n\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'uptime::livewire.charts.latency-chart';\n\n        return view($view, [\n            'identifier' => $this->getIdentifier(),\n            'height' => $this->height,\n            'availableCountries' => $this->availableCountries(),\n            'closestCountry' => $this->getClosestCountry(),\n            'dateRangeOptions' => $this->getDateRangeOptions(),\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Http/Livewire/Forms/CreateUptimeMonitorForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Http\\Livewire\\Forms;\n\nuse Illuminate\\Validation\\Rule;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Form;\nuse Vigilant\\Core\\Validation\\CanEnableRule;\nuse Vigilant\\Frontend\\Validation\\CountryCode;\nuse Vigilant\\Uptime\\Enums\\Type;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass CreateUptimeMonitorForm extends Form\n{\n    #[Locked]\n    public ?int $site_id;\n\n    #[Validate('required|max:255')]\n    public string $name = '';\n\n    public bool $enabled = true;\n\n    public string $type = Type::Http->value;\n\n    public array $settings = [\n        'host' => '',\n    ];\n\n    public int $interval = 60;\n\n    #[Validate('required|integer|min:0|max:3')]\n    public ?int $retries = 0;\n\n    #[Validate('required|integer|max:10')]\n    public ?int $timeout = 5;\n\n    public bool $geoip_automatic = true;\n\n    public ?string $country = null;\n\n    public ?float $latitude = null;\n\n    public ?float $longitude = null;\n\n    public function getRules(): array\n    {\n        return array_merge(parent::getRules(),\n            [\n                'type' => ['required', Rule::enum(Type::class)],\n                'name' => ['required', 'string', 'max:255'],\n                'interval' => ['required', 'integer', 'in:'.implode(',', array_keys(config('uptime.intervals')))],\n                'settings.port' => ['integer', 'min:1', 'max:65535', sprintf('required_if:type,%s', Type::Tcp->value)],\n                'settings.host' => [sprintf('required_if:type,%s,%s,%s', Type::Http->value, Type::Ping->value, Type::Tcp->value)],\n                'enabled' => ['boolean', new CanEnableRule(Monitor::class)],\n                'geoip_automatic' => ['boolean'],\n                'country' => [\n                    Rule::requiredIf(fn () => ! $this->geoip_automatic),\n                    'nullable',\n                    'string',\n                    new CountryCode,\n                ],\n                'latitude' => [\n                    Rule::requiredIf(fn () => ! $this->geoip_automatic),\n                    'nullable',\n                    'numeric',\n                    'between:-90,90',\n                ],\n                'longitude' => [\n                    Rule::requiredIf(fn () => ! $this->geoip_automatic),\n                    'nullable',\n                    'numeric',\n                    'between:-180,180',\n                ],\n            ]);\n    }\n\n    public function all(): array\n    {\n        $values = parent::all();\n\n        if ($this->geoip_automatic) {\n            $values['country'] = null;\n            $values['latitude'] = null;\n            $values['longitude'] = null;\n        } elseif ($values['country'] !== null) {\n            $values['country'] = strtoupper(trim($values['country']));\n        }\n\n        return $values;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Http/Livewire/Monitor/Dashboard.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Http\\Livewire\\Monitor;\n\nuse Illuminate\\View\\View;\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\nuse Vigilant\\Uptime\\Actions\\CalculateUptimePercentage;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass Dashboard extends Component\n{\n    #[Locked]\n    public int $monitorId;\n\n    public function mount(int $monitorId): void\n    {\n        $this->monitorId = $monitorId;\n    }\n\n    public function render(): View\n    {\n        $uptimePercentage = app(CalculateUptimePercentage::class);\n\n        /** @var Monitor $monitor */\n        $monitor = Monitor::query()->findOrFail($this->monitorId);\n\n        /** @var view-string $view */\n        $view = 'uptime::livewire.monitor.dashboard';\n\n        return view($view, [\n            'monitor' => $monitor,\n            'lastDowntime' => $monitor->downtimes()\n                ->whereNotNull('end')\n                ->orderByDesc('start')\n                ->first(),\n            'uptime30d' => $uptimePercentage->calculate($monitor, '-30 days'),\n            'uptime7d' => $uptimePercentage->calculate($monitor, '-7 days'),\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Http/Livewire/Tables/DowntimeTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Http\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Livewire\\Attributes\\Locked;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\DateColumn;\nuse Vigilant\\Uptime\\Models\\Downtime;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass DowntimeTable extends BaseTable\n{\n    protected string $model = Downtime::class;\n\n    #[Locked]\n    public int $monitorId = 0;\n\n    public string $sortColumn = 'start';\n\n    public string $sortDirection = 'desc';\n\n    public function mount(int $monitorId): void\n    {\n        $this->monitorId = $monitorId;\n        Monitor::query()->findOrFail($monitorId);\n    }\n\n    protected function columns(): array\n    {\n        return [\n\n            DateColumn::make(__('Start'), 'start')\n                ->sortable(),\n\n            DateColumn::make(__('End'), 'end')\n                ->sortable(),\n\n            Column::make(__('Duration'), function (Downtime $downtime) {\n                if ($downtime->end === null) {\n                    return __('Ongoing');\n                }\n\n                return teamTimezone($downtime->start)->longAbsoluteDiffForHumans(teamTimezone($downtime->end));\n            }),\n        ];\n    }\n\n    protected function query(): Builder\n    {\n        return parent::query()\n            ->where('monitor_id', '=', $this->monitorId);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Http/Livewire/Tables/MonitorTable.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Http\\Livewire\\Tables;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Enumerable;\nuse Illuminate\\Support\\Facades\\Gate;\nuse RamonRietdijk\\LivewireTables\\Actions\\Action;\nuse RamonRietdijk\\LivewireTables\\Columns\\Column;\nuse RamonRietdijk\\LivewireTables\\Filters\\SelectFilter;\nuse Vigilant\\Frontend\\Integrations\\Table\\BaseTable;\nuse Vigilant\\Frontend\\Integrations\\Table\\ChartColumn;\nuse Vigilant\\Frontend\\Integrations\\Table\\Enums\\Status;\nuse Vigilant\\Frontend\\Integrations\\Table\\StatusColumn;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Uptime\\Actions\\CalculateUptimePercentage;\nuse Vigilant\\Uptime\\Models\\Downtime;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Models\\Result;\nuse Vigilant\\Uptime\\Models\\ResultAggregate;\n\nclass MonitorTable extends BaseTable\n{\n    protected string $model = Monitor::class;\n\n    protected array $pollingOptions = [\n        '' => 'None',\n        '30s' => 'Every 30 seconds',\n    ];\n\n    protected function columns(): array\n    {\n        /** @var CalculateUptimePercentage $calculateUptime */\n        $calculateUptime = app(CalculateUptimePercentage::class);\n\n        return [\n            StatusColumn::make(__('Status'))\n                ->text(function (Monitor $monitor): string {\n                    if (! $monitor->enabled) {\n                        return __('Disabled');\n                    }\n\n                    $downtime = $monitor->currentDowntime();\n\n                    if ($downtime !== null) {\n                        return __('Down');\n                    }\n\n                    /** @var null|Result|ResultAggregate $lastResult */\n                    $lastResult = $monitor->results()->orderByDesc('created_at')->first();\n\n                    if ($lastResult === null) {\n                        $lastResult = $monitor->aggregatedResults()->orderByDesc('created_at')->first();\n                    }\n\n                    if ($lastResult === null || ! isset($lastResult->created_at)) {\n                        return __('Unknown');\n                    }\n\n                    if ($lastResult->created_at->lessThan(now()->subMinutes(5))) {\n                        return __('Last check: :time', ['time' => $lastResult->created_at->diffForHumans()]);\n                    }\n\n                    return __('Up');\n                })\n                ->status(function (Monitor $monitor): Status {\n                    $downtime = $monitor->currentDowntime();\n\n                    if ($downtime !== null || ! $monitor->enabled) {\n                        return Status::Danger;\n                    }\n\n                    /** @var null|Result|ResultAggregate $lastResult */\n                    $lastResult = $monitor->results()->orderByDesc('created_at')->first() ?? $monitor->aggregatedResults()->orderByDesc('created_at')->first();\n\n                    if ($lastResult === null || $lastResult->created_at === null || $lastResult->created_at->lessThan(now()->subMinutes(5))) {\n                        return Status::Warning;\n                    }\n\n                    return Status::Success;\n                }),\n\n            Column::make(__('Name'), 'name')\n                ->searchable()\n                ->sortable(),\n\n            ChartColumn::make(__('Latency'))\n                ->component('monitor-column-latency-chart')\n                ->parameters(fn (Monitor $monitor) => ['monitorId' => $monitor->id]),\n\n            Column::make(__('Uptime'))\n                ->displayUsing(function (Monitor $monitor) use ($calculateUptime) {\n\n                    $percentage = $calculateUptime->calculate($monitor);\n\n                    if ($percentage === null) {\n                        return __('Not available yet');\n                    }\n\n                    $class = match (true) {\n                        $percentage > 95 => 'text-green-light',\n                        $percentage > 80 => 'text-orange',\n                        default => 'text-red'\n                    };\n\n                    return \"<span class='$class'>\".number_format($percentage, 2).'%</span>';\n                })\n                ->asHtml(),\n\n            Column::make(__('Last downtime'))\n                ->displayUsing(function (Monitor $monitor) {\n                    /** @var ?Downtime $lastDowntime */\n                    $lastDowntime = $monitor->downtimes()\n                        ->whereNotNull('end')\n                        ->orderByDesc('start')\n                        ->first();\n\n                    if ($lastDowntime === null) {\n                        return __('Never');\n                    }\n\n                    return teamTimezone($lastDowntime->start)->diffForHumans();\n                }),\n        ];\n    }\n\n    protected function filters(): array\n    {\n        return [\n            SelectFilter::make(__('Site'), 'site_id')\n                ->options(\n                    Site::query()\n                        ->orderBy('url')\n                        ->pluck('url', 'id')\n                        ->toArray()\n                ),\n        ];\n    }\n\n    protected function actions(): array\n    {\n        return [\n            Action::make(__('Enable'), function (Enumerable $models): void {\n                foreach ($models as $model) {\n                    if (! Gate::allows('create', $model)) {\n                        break;\n                    }\n\n                    $model->update(['enabled' => true]);\n                }\n            }, 'enable'),\n\n            Action::make(__('Disable'), function (Enumerable $models): void {\n                $models->each(fn (Monitor $monitor) => $monitor->update(['enabled' => false]));\n            }, 'disable'),\n\n            Action::make(__('Delete'), function (Enumerable $models): void {\n                $models->each(fn (Monitor $monitor) => $monitor->delete());\n            }, 'delete'),\n        ];\n    }\n\n    public function link(Model $model): ?string\n    {\n        return route('uptime.monitor.view', ['monitor' => $model]);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Http/Livewire/UptimeMonitorForm.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Http\\Livewire;\n\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Attributes\\On;\nuse Livewire\\Component;\nuse Vigilant\\Frontend\\Concerns\\DisplaysAlerts;\nuse Vigilant\\Frontend\\Enums\\AlertType;\nuse Vigilant\\Frontend\\Traits\\CanBeInline;\nuse Vigilant\\Uptime\\Http\\Livewire\\Forms\\CreateUptimeMonitorForm;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass UptimeMonitorForm extends Component\n{\n    use CanBeInline;\n    use DisplaysAlerts;\n\n    public CreateUptimeMonitorForm $form;\n\n    #[Locked]\n    public Monitor $monitor;\n\n    public function mount(?Monitor $monitor): void\n    {\n        if ($monitor !== null) {\n            if ($monitor->exists) {\n                $this->authorize('update', $monitor);\n            } else {\n                $this->authorize('create', $monitor);\n            }\n\n            $this->form->fill($monitor->toArray());\n            $this->monitor = $monitor;\n        }\n\n        /** @var array<int, int> $availableIntervals */\n        $availableIntervals = array_keys(config('uptime.intervals', []));\n\n        if (! in_array($this->form->interval, $availableIntervals) && count($availableIntervals) > 0) {\n            $this->form->interval = $availableIntervals[0];\n        }\n    }\n\n    #[On('save')]\n    public function save(): void\n    {\n        $this->validate();\n\n        if ($this->monitor->exists) {\n            $this->authorize('update', $this->monitor);\n\n            $this->monitor->update($this->form->all());\n        } else {\n            $this->authorize('create', $this->monitor);\n\n            $this->monitor = Monitor::query()->create(\n                $this->form->all()\n            );\n        }\n\n        if (! $this->inline) {\n            $this->alert(\n                __('Saved'),\n                __('Uptime monitor was successfully :action',\n                    ['action' => $this->monitor->wasRecentlyCreated ? 'created' : 'saved']),\n                AlertType::Success\n            );\n            $this->redirectRoute('uptime');\n        }\n    }\n\n    public function render(): mixed\n    {\n        /** @var view-string $view */\n        $view = 'uptime::livewire.monitor.form';\n\n        return view($view, [\n            'updating' => $this->monitor->exists,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Http/Livewire/UptimeMonitors.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Http\\Livewire;\n\nuse Illuminate\\View\\View;\nuse Livewire\\Component;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass UptimeMonitors extends Component\n{\n    public function render(): View\n    {\n        /** @var view-string $view */\n        $view = 'uptime::livewire.uptime-monitors';\n        $hasMonitors = Monitor::query()->exists();\n\n        return view($view, [\n            'hasMonitors' => $hasMonitors,\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Http/Middleware/ExternalOutpostMiddleware.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Http\\Middleware;\n\nuse Illuminate\\Http\\Request;\n\nclass ExternalOutpostMiddleware\n{\n    public function handle(Request $request, \\Closure $next): mixed\n    {\n        $allowExternalOutposts = config('uptime.allow_external_outposts', false);\n\n        $ip = $request->ip();\n\n        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {\n            if (! $allowExternalOutposts) {\n                return response()->json(['message' => 'External outposts are not allowed.'], 403);\n            }\n        }\n\n        return $next($request);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Http/Middleware/OutpostAuthMiddleware.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Http\\Middleware;\n\nuse Illuminate\\Http\\Request;\n\nclass OutpostAuthMiddleware\n{\n    public function handle(Request $request, \\Closure $next): mixed\n    {\n        $token = $request->header('X-Outpost-Token');\n\n        if ($token !== config('uptime.outpost_token')) {\n            return response()->json(['message' => 'Unauthorized'], 401);\n        }\n\n        return $next($request);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Jobs/AggregateResultsJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Uptime\\Actions\\AggregateResults;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass AggregateResultsJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(public Monitor $monitor)\n    {\n        $this->onQueue(config('uptime.queue'));\n    }\n\n    public function handle(AggregateResults $results): void\n    {\n        $results->aggregate($this->monitor);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->monitor->id;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Jobs/CheckUnavailableOutpostJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUnique;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Http\\Client\\ConnectionException;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Uptime\\Enums\\OutpostStatus;\nuse Vigilant\\Uptime\\Models\\Outpost;\n\nclass CheckUnavailableOutpostJob implements ShouldBeUnique, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(public Outpost $outpost)\n    {\n        $this->onQueue(config('uptime.queue'));\n    }\n\n    public function handle(): void\n    {\n        try {\n            $response = Http::timeout(5)\n                ->connectTimeout(5)\n                ->baseUrl($this->outpost->url())\n                ->get('health');\n\n            if ($response->successful()) {\n                $this->outpost->update([\n                    'status' => OutpostStatus::Available,\n                    'unavailable_at' => null,\n                    'last_available_at' => now(),\n                ]);\n            } else {\n                $this->outpost->delete();\n            }\n        } catch (ConnectionException) {\n            $this->outpost->delete();\n        }\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->outpost->id;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Jobs/CheckUptimeJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldBeUniqueUntilProcessing;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Uptime\\Actions\\CheckUptime;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass CheckUptimeJob implements ShouldBeUniqueUntilProcessing, ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(public Monitor $monitor)\n    {\n        $this->onQueue(config('uptime.queue'));\n    }\n\n    public function handle(CheckUptime $uptime, TeamService $teamService): void\n    {\n        $teamService->setTeamById($this->monitor->team_id);\n        $uptime->check($this->monitor);\n    }\n\n    public function uniqueId(): int\n    {\n        return $this->monitor->id;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Jobs/UpdateMonitorLocationJob.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\nuse Vigilant\\Uptime\\Actions\\FetchGeolocation;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass UpdateMonitorLocationJob implements ShouldQueue\n{\n    use Dispatchable;\n    use InteractsWithQueue;\n    use Queueable;\n    use SerializesModels;\n\n    public function __construct(public Monitor $monitor)\n    {\n        $this->onQueue(config('uptime.queue'));\n    }\n\n    public function handle(FetchGeolocation $fetchGeolocation): void\n    {\n        if (! $this->monitor->shouldFetchGeoip()) {\n            return;\n        }\n\n        $target = $this->monitor->type->formatTarget($this->monitor);\n\n        $geolocation = $fetchGeolocation->fetch($target);\n\n        if ($geolocation === null) {\n            return;\n        }\n\n        $this->monitor->updateQuietly([\n            'country' => $geolocation['country'],\n            'latitude' => $geolocation['latitude'],\n            'longitude' => $geolocation['longitude'],\n            'geoip_fetched_at' => now(),\n        ]);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Listeners/CheckLatencyListener.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Listeners;\n\nuse Vigilant\\Uptime\\Actions\\CheckLatency;\nuse Vigilant\\Uptime\\Events\\UptimeCheckedEvent;\n\nclass CheckLatencyListener\n{\n    public function __construct(protected CheckLatency $checker) {}\n\n    public function handle(UptimeCheckedEvent $event): void\n    {\n        $this->checker->check($event->monitor);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Listeners/DowntimeEndNotificationListener.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Listeners;\n\nuse Vigilant\\Uptime\\Events\\DowntimeEndEvent;\nuse Vigilant\\Uptime\\Notifications\\DowntimeEndNotification;\n\nclass DowntimeEndNotificationListener\n{\n    public function handle(DowntimeEndEvent $event): void\n    {\n        DowntimeEndNotification::notify($event->downtime);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Listeners/DowntimeStartNotificationListener.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Listeners;\n\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Uptime\\Events\\DowntimeStartEvent;\nuse Vigilant\\Uptime\\Notifications\\DowntimeStartNotification;\n\nclass DowntimeStartNotificationListener\n{\n    public function __construct(protected TeamService $teamService) {}\n\n    public function handle(DowntimeStartEvent $event): void\n    {\n        $this->teamService->setTeam($event->monitor->team);\n\n        DowntimeStartNotification::notify($event->monitor);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Models/Downtime.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Prunable;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Concerns\\HasDataRetention;\n\n/**\n * @property int $id\n * @property int $checker_id\n * @property Carbon $start\n * @property ?Carbon $end\n * @property ?array $data\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Monitor $monitor\n */\nclass Downtime extends Model\n{\n    use HasDataRetention;\n    use Prunable;\n\n    protected $table = 'uptime_downtimes';\n\n    protected $guarded = [];\n\n    protected $casts = [\n        'start' => 'datetime',\n        'end' => 'datetime',\n        'data' => 'array',\n    ];\n\n    public function monitor(): BelongsTo\n    {\n        return $this->belongsTo(Monitor::class);\n    }\n\n    public function prunable(): Builder\n    {\n        return static::withoutGlobalScopes()->where('created_at', '<=', $this->retentionPeriod());\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Models/Monitor.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Attributes\\ScopedBy;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Support\\Carbon;\nuse Illuminate\\Support\\Collection;\nuse Vigilant\\Core\\Scopes\\TeamScope;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Uptime\\Database\\Factories\\MonitorFactory;\nuse Vigilant\\Uptime\\Enums\\State;\nuse Vigilant\\Uptime\\Enums\\Type;\nuse Vigilant\\Uptime\\Observers\\MonitorObserver;\nuse Vigilant\\Users\\Models\\Team;\n\n/**\n * @property int $id\n * @property bool $enabled\n * @property ?int $site_id\n * @property int $team_id\n * @property string $name\n * @property State $state\n * @property int $try\n * @property Type $type\n * @property array $settings\n * @property ?Carbon $next_run\n * @property int $interval\n * @property int $retries\n * @property int $timeout\n * @property ?string $country\n * @property ?float $latitude\n * @property ?float $longitude\n * @property ?int $closest_outpost_id\n * @property ?Carbon $geoip_fetched_at\n * @property bool $geoip_automatic\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Site $site\n * @property ?Team $team\n * @property ?Outpost $closestOutpost\n * @property Collection<int, Result> $results\n * @property Collection<int, Result> $aggregatedResults\n * @property Collection<int, Downtime> $downtimes\n */\n#[ObservedBy([MonitorObserver::class])]\n#[ScopedBy([TeamScope::class])]\nclass Monitor extends Model\n{\n    use HasFactory;\n\n    protected $table = 'uptime_monitors';\n\n    protected $guarded = [];\n\n    protected $casts = [\n        'enabled' => 'boolean',\n        'type' => Type::class,\n        'settings' => 'array',\n        'next_run' => 'datetime',\n        'state' => State::class,\n        'interval' => 'integer',\n        'latitude' => 'float',\n        'longitude' => 'float',\n        'geoip_fetched_at' => 'datetime',\n        'geoip_automatic' => 'boolean',\n    ];\n\n    public function site(): BelongsTo\n    {\n        return $this->belongsTo(Site::class);\n    }\n\n    public function results(): HasMany\n    {\n        return $this->hasMany(Result::class);\n    }\n\n    public function aggregatedResults(): HasMany\n    {\n        return $this->hasMany(ResultAggregate::class);\n    }\n\n    public function downtimes(): HasMany\n    {\n        return $this->hasMany(Downtime::class);\n    }\n\n    public function team(): BelongsTo\n    {\n        return $this->belongsTo(Team::class);\n    }\n\n    public function closestOutpost(): BelongsTo\n    {\n        return $this->belongsTo(Outpost::class, 'closest_outpost_id');\n    }\n\n    public function currentDowntime(): ?Downtime\n    {\n        /** @var ?Downtime $downtime */\n        $downtime = $this->downtimes()\n            ->whereNull('end')\n            ->orderByDesc('start')\n            ->first();\n\n        return $downtime;\n    }\n\n    public function shouldFetchGeoip(): bool\n    {\n        if (! $this->geoip_automatic) {\n            return false;\n        }\n\n        if ($this->geoip_fetched_at === null) {\n            return true;\n        }\n\n        return $this->geoip_fetched_at->lt(now()->subDay());\n    }\n\n    protected static function newFactory(): MonitorFactory\n    {\n        return new MonitorFactory;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Models/Outpost.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Attributes\\ObservedBy;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Uptime\\Enums\\OutpostStatus;\nuse Vigilant\\Uptime\\Observers\\OutpostObserver;\n\n/**\n * @property int $id\n * @property string $ip\n * @property int $port\n * @property string $external_ip\n * @property OutpostStatus $status\n * @property ?string $country\n * @property ?float $latitude\n * @property ?float $longitude\n * @property bool $geoip_automatic\n * @property Carbon $last_available_at\n * @property ?Carbon $unavailable_at\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n */\n#[ObservedBy([OutpostObserver::class])]\nclass Outpost extends Model\n{\n    protected $table = 'uptime_outposts';\n\n    protected $guarded = [];\n\n    protected $casts = [\n        'status' => OutpostStatus::class,\n        'latitude' => 'float',\n        'longitude' => 'float',\n        'geoip_automatic' => 'boolean',\n        'last_available_at' => 'datetime',\n        'unavailable_at' => 'datetime',\n    ];\n\n    public function url(): string\n    {\n        return \"https://{$this->ip}:{$this->port}\";\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Models/Result.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Prunable;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Core\\Concerns\\HasDataRetention;\n\n/**\n * @property int $id\n * @property int $checker_id\n * @property int $total_time\n * @property ?string $country\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Monitor $monitor\n */\nclass Result extends Model\n{\n    use HasDataRetention;\n    use Prunable;\n\n    protected $table = 'uptime_results';\n\n    protected $guarded = [];\n\n    public function monitor(): BelongsTo\n    {\n        return $this->belongsTo(Monitor::class);\n    }\n\n    public function prunable(): Builder\n    {\n        return static::withoutGlobalScopes()->where('created_at', '<=', $this->retentionPeriod());\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Models/ResultAggregate.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Support\\Carbon;\n\n/**\n * @property int $id\n * @property int $checker_id\n * @property int $total_time\n * @property ?string $country\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Monitor $monitor\n */\nclass ResultAggregate extends Model\n{\n    protected $table = 'uptime_results_aggregates';\n\n    protected $guarded = [];\n\n    public function monitor(): BelongsTo\n    {\n        return $this->belongsTo(Monitor::class);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Notifications/Conditions/ClosestCountryCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Notifications\\Conditions;\n\nuse Vigilant\\Notifications\\Conditions\\StaticCondition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Uptime\\Notifications\\LatencyChangedNotification;\nuse Vigilant\\Uptime\\Notifications\\LatencyPeakNotification;\n\nclass ClosestCountryCondition extends StaticCondition\n{\n    public static string $name = 'Only from closest country';\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var LatencyChangedNotification|LatencyPeakNotification $notification */\n        $country = $notification->country;\n\n        if ($country === null) {\n            return false;\n        }\n\n        $closestCountry = $notification->monitor->closestOutpost?->country;\n\n        if ($closestCountry === null) {\n            return false;\n        }\n\n        return $country === $closestCountry;\n    }\n\n    public static function info(): ?string\n    {\n        return __('Only triggers when the notification originates from your geographically closest monitoring location.');\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Notifications/Conditions/CountryCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Notifications\\Conditions;\n\nuse Vigilant\\Notifications\\Conditions\\SelectCondition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Uptime\\Models\\Outpost;\nuse Vigilant\\Uptime\\Notifications\\LatencyChangedNotification;\nuse Vigilant\\Uptime\\Notifications\\LatencyPeakNotification;\n\nclass CountryCondition extends SelectCondition\n{\n    public static string $name = 'Country';\n\n    public function options(): array\n    {\n        return Outpost::query()\n            ->whereNotNull('country')\n            ->distinct('country')\n            ->orderBy('country')\n            ->pluck('country', 'country')\n            ->toArray();\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'is',\n            '!=' => 'is not',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var LatencyChangedNotification|LatencyPeakNotification $notification */\n        $country = $notification->country;\n\n        if ($country === null) {\n            return false;\n        }\n\n        return match ($operator) {\n            '=' => $country == $value,\n            '!=' => $country != $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Notifications/Conditions/LatencyMsCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Notifications\\Conditions;\n\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Enums\\ConditionType;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Uptime\\Notifications\\LatencyChangedNotification;\nuse Vigilant\\Uptime\\Notifications\\LatencyPeakNotification;\n\nclass LatencyMsCondition extends Condition\n{\n    public static string $name = 'Latency (ms)';\n\n    public ConditionType $type = ConditionType::Number;\n\n    public function operands(): array\n    {\n        return [\n            'current' => 'Current',\n            'change' => 'Change',\n            'change_absolute' => 'Change (absolute)',\n        ];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '<>' => 'Not equal to',\n            '<' => 'Less than',\n            '<=' => 'Less or equal than',\n            '>' => 'Greater than',\n            '>=' => 'Greater or equal than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var LatencyChangedNotification|LatencyPeakNotification $notification */\n        $msValue = match ($operand) {\n            'current' => $this->getCurrentLatency($notification),\n            'change' => $this->getLatencyChange($notification),\n            'change_absolute' => abs($this->getLatencyChange($notification)),\n            default => 0,\n        };\n\n        return match ($operator) {\n            '=' => $msValue == $value,\n            '<>' => $msValue != $value,\n            '<' => $msValue < $value,\n            '<=' => $msValue <= $value,\n            '>' => $msValue > $value,\n            '>=' => $msValue >= $value,\n            default => false,\n        };\n    }\n\n    protected function getCurrentLatency(LatencyChangedNotification|LatencyPeakNotification $notification): float\n    {\n        if ($notification instanceof LatencyChangedNotification) {\n            return $notification->currentAverage;\n        }\n\n        return $notification->peakLatency;\n    }\n\n    protected function getLatencyChange(LatencyChangedNotification|LatencyPeakNotification $notification): float\n    {\n        if ($notification instanceof LatencyChangedNotification) {\n            return $notification->currentAverage - $notification->previousAverage;\n        }\n\n        return $notification->peakLatency - $notification->averageLatency;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Notifications/Conditions/LatencyPercentCondition.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Notifications\\Conditions;\n\nuse Vigilant\\Notifications\\Conditions\\Condition;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Uptime\\Notifications\\LatencyChangedNotification;\n\nclass LatencyPercentCondition extends Condition\n{\n    public static string $name = 'Percent change';\n\n    public function operands(): array\n    {\n        return [\n            'relative' => 'Relative',\n            'absolute' => 'Absolute',\n        ];\n    }\n\n    public function operators(): array\n    {\n        return [\n            '=' => 'Equal to',\n            '<>' => 'Not equal to',\n            '<' => 'Less than',\n            '<=' => 'Less or equal than',\n            '>' => 'Greater than',\n            '>=' => 'Greater or equal than',\n        ];\n    }\n\n    public function applies(\n        Notification $notification,\n        ?string $operand,\n        ?string $operator,\n        mixed $value,\n        ?array $meta\n    ): bool {\n        /** @var LatencyChangedNotification $notification */\n        $percentChanged = $notification->percent;\n\n        if ($operand === 'absolute') {\n            $percentChanged = abs($percentChanged);\n        }\n\n        return match ($operator) {\n            '=' => $percentChanged == $value,\n            '<>' => $percentChanged != $value,\n            '<' => $percentChanged < $value,\n            '<=' => $percentChanged <= $value,\n            '>' => $percentChanged > $value,\n            '>=' => $percentChanged >= $value,\n            default => false,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Notifications/DowntimeEndNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Notifications;\n\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Uptime\\Models\\Downtime;\n\nclass DowntimeEndNotification extends Notification implements HasSite\n{\n    public static string $name = 'Downtime solved';\n\n    public Level $level = Level::Success;\n\n    public function __construct(\n        public Downtime $downtime\n    ) {}\n\n    public function title(): string\n    {\n        $monitor = $this->downtime->monitor;\n\n        $site = $monitor->site->url ?? $monitor->settings['host'] ?? '';\n\n        return __(':site is back up!', ['site' => $site]);\n    }\n\n    public function description(): string\n    {\n        return __('When down at :start and became available on :end. Downtime: :downtime', [\n            'start' => $this->downtime->start->toDateTimeString(),\n            'end' => $this->downtime->end?->toDateTimeString() ?? __('Unknown'),\n            'downtime' => $this->downtime->start->longAbsoluteDiffForHumans($this->downtime->end),\n        ]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when your site recovers and becomes available again after downtime.');\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->downtime->id;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->downtime->monitor?->site;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Notifications/DowntimeStartNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Notifications;\n\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass DowntimeStartNotification extends Notification implements HasSite\n{\n    public static string $name = 'Downtime detected';\n\n    public Level $level = Level::Critical;\n\n    public function __construct(\n        public Monitor $monitor\n    ) {}\n\n    public function title(): string\n    {\n        $host = $this->monitor->site->url ?? $this->monitor->settings['host'] ?? '';\n\n        return __(':host is down!', ['host' => $host]);\n    }\n\n    public function description(): string\n    {\n        $downtime = $this->monitor->currentDowntime();\n\n        if ($downtime === null) {\n            return '';\n        }\n\n        return __('Since: :start', ['start' => $downtime->start->toDateTimeString()]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when your site becomes unreachable.');\n    }\n\n    public function uniqueId(): string\n    {\n        return (string) $this->monitor->id;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->monitor->site;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Notifications/LatencyChangedNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Notifications;\n\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Notifications\\Conditions\\LatencyPercentCondition;\n\nclass LatencyChangedNotification extends Notification implements HasSite\n{\n    public static string $name = 'Latency Changed';\n\n    public static ?int $defaultCooldown = 60 * 24;\n\n    public Level $level = Level::Warning;\n\n    public static array $defaultConditions = [\n        'type' => 'group',\n        'operator' => 'all',\n        'children' => [\n            [\n                'type' => 'condition',\n                'condition' => LatencyPercentCondition::class,\n                'operator' => '>=',\n                'operand' => 'absolute',\n                'value' => 50,\n            ],\n        ],\n    ];\n\n    public function __construct(\n        public Monitor $monitor,\n        public float $percent,\n        public float $previousAverage,\n        public float $currentAverage,\n        public ?string $country = null\n    ) {}\n\n    public function title(): string\n    {\n        $site = $this->site()->url ?? $this->monitor->settings['host'] ?? '';\n        $country = $this->country ? \" in {$this->country}\" : '';\n\n        return __(':site latency changed by :percent % from :country', ['site' => $site, 'percent' => $this->percent, 'country' => $country]);\n    }\n\n    public function description(): string\n    {\n        if ($this->country) {\n            return __('Past 12 hour average: :previous ms. Current average: :current ms from :country', [\n                'previous' => round($this->previousAverage, 2),\n                'current' => round($this->currentAverage, 2),\n                'country' => $this->country,\n            ]);\n        } else {\n            return __('Past 12 hour average: :previous ms. Current average: :current ms', [\n                'previous' => round($this->previousAverage, 2),\n                'current' => round($this->currentAverage, 2),\n            ]);\n        }\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered after an uptime check if the latency has changed.');\n    }\n\n    public function viewUrl(): ?string\n    {\n        return route('uptime.monitor.view', ['monitor' => $this->monitor]);\n    }\n\n    public function level(): Level\n    {\n        return match (true) {\n            $this->percent < 100 => Level::Info,\n            $this->percent < 200 => Level::Warning,\n            default => Level::Critical,\n        };\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->country\n            ? \"{$this->monitor->id}_{$this->country}\"\n            : $this->monitor->id;\n    }\n\n    public function site(): ?Site\n    {\n        return $this->monitor->site;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Notifications/LatencyPeakNotification.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Notifications;\n\nuse Vigilant\\Notifications\\Contracts\\HasSite;\nuse Vigilant\\Notifications\\Enums\\Level;\nuse Vigilant\\Notifications\\Notifications\\Notification;\nuse Vigilant\\Sites\\Models\\Site;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Notifications\\Conditions\\ClosestCountryCondition;\nuse Vigilant\\Uptime\\Notifications\\Conditions\\LatencyMsCondition;\nuse Vigilant\\Uptime\\Notifications\\Conditions\\LatencyPercentCondition;\n\nclass LatencyPeakNotification extends Notification implements HasSite\n{\n    public static string $name = 'Latency Peak';\n\n    public static ?int $defaultCooldown = 60 * 6; // 6 hours\n\n    public Level $level = Level::Warning;\n\n    public static bool $autoCreate = false;\n\n    public static array $defaultConditions = [\n        'type' => 'group',\n        'operator' => 'all',\n        'children' => [\n            [\n                'type' => 'condition',\n                'condition' => LatencyPercentCondition::class,\n                'operator' => '>=',\n                'operand' => 'absolute',\n                'value' => 100,\n            ],\n            [\n                'type' => 'condition',\n                'condition' => LatencyMsCondition::class,\n                'operator' => '>=',\n                'operand' => 'change_absolute',\n                'value' => 500,\n            ],\n            [\n                'type' => 'condition',\n                'condition' => ClosestCountryCondition::class,\n            ],\n        ],\n    ];\n\n    public function __construct(\n        public Monitor $monitor,\n        public float $peakLatency,\n        public float $averageLatency,\n        public float $percent,\n        public ?string $country = null\n    ) {}\n\n    public function title(): string\n    {\n        $site = $this->site()->url ?? $this->monitor->settings['host'] ?? '';\n        if ($this->country) {\n            return __(':site latency is peaking from :country', ['site' => $site, 'country' => $this->country]);\n        } else {\n            return __(':site latency is peaking', ['site' => $site]);\n        }\n    }\n\n    public function description(): string\n    {\n        $country = $this->country ? \" ({$this->country})\" : '';\n\n        return __('Current peak: :peak ms. Average: :average ms (+:percent%) from :country', [\n            'peak' => round($this->peakLatency, 2),\n            'average' => round($this->averageLatency, 2),\n            'percent' => round($this->percent, 0),\n            'country' => $country,\n        ]);\n    }\n\n    public static function info(): ?string\n    {\n        return __('Triggered when latency spikes significantly above the average in the past hour.');\n    }\n\n    public function viewUrl(): ?string\n    {\n        return route('uptime.monitor.view', ['monitor' => $this->monitor]);\n    }\n\n    public function level(): Level\n    {\n        return match (true) {\n            $this->percent < 100 => Level::Info,\n            $this->percent < 200 => Level::Warning,\n            default => Level::Critical,\n        };\n    }\n\n    public function uniqueId(): string|int\n    {\n        return $this->country\n            ? \"peak_{$this->monitor->id}_{$this->country}\"\n            : \"peak_{$this->monitor->id}\";\n    }\n\n    public function site(): ?Site\n    {\n        return $this->monitor->site;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Observers/MonitorObserver.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Observers;\n\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Uptime\\Jobs\\CheckUptimeJob;\nuse Vigilant\\Uptime\\Jobs\\UpdateMonitorLocationJob;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass MonitorObserver\n{\n    public function creating(Monitor $monitor): void\n    {\n        /** @var TeamService $teamService */\n        $teamService = app(TeamService::class);\n\n        $team = $teamService->team();\n\n        $monitor->team_id = $team->id;\n    }\n\n    public function created(Monitor $monitor): void\n    {\n        UpdateMonitorLocationJob::dispatch($monitor);\n        CheckUptimeJob::dispatch($monitor);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/Observers/OutpostObserver.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Observers;\n\nuse Vigilant\\Uptime\\Enums\\OutpostStatus;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Models\\Outpost;\n\nclass OutpostObserver\n{\n    public function updated(Outpost $outpost): void\n    {\n        // If outpost becomes unavailable, clear it from monitors using it as closest outpost\n        if ($outpost->status === OutpostStatus::Unavailable) {\n            Monitor::query()\n                ->withoutGlobalScopes()\n                ->where('closest_outpost_id', $outpost->id)\n                ->update(['closest_outpost_id' => null]);\n\n            // Set unavailable_at if not already set\n            if ($outpost->unavailable_at === null) {\n                $outpost->updateQuietly(['unavailable_at' => now()]);\n            }\n        }\n    }\n\n    public function deleted(Outpost $outpost): void\n    {\n        // Clear the closest_outpost_id for monitors using this outpost\n        Monitor::query()\n            ->withoutGlobalScopes()\n            ->where('closest_outpost_id', $outpost->id)\n            ->update(['closest_outpost_id' => null]);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime;\n\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\RateLimiter;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\nuse Livewire\\Livewire;\nuse Vigilant\\Core\\Facades\\Navigation;\nuse Vigilant\\Core\\Policies\\AllowAllPolicy;\nuse Vigilant\\Notifications\\Facades\\NotificationRegistry;\nuse Vigilant\\Uptime\\Commands\\AggregateResultsCommand;\nuse Vigilant\\Uptime\\Commands\\CheckLatencyCommand;\nuse Vigilant\\Uptime\\Commands\\CheckUnavailableOutpostsCommand;\nuse Vigilant\\Uptime\\Commands\\CheckUptimeCommand;\nuse Vigilant\\Uptime\\Commands\\GenerateRootCaCommand;\nuse Vigilant\\Uptime\\Commands\\ScheduleUptimeChecksCommand;\nuse Vigilant\\Uptime\\Events\\DowntimeEndEvent;\nuse Vigilant\\Uptime\\Events\\DowntimeStartEvent;\nuse Vigilant\\Uptime\\Events\\UptimeCheckedEvent;\nuse Vigilant\\Uptime\\Http\\Livewire\\Charts\\ColumnLatencyChart;\nuse Vigilant\\Uptime\\Http\\Livewire\\Charts\\LatencyChart;\nuse Vigilant\\Uptime\\Http\\Livewire\\Monitor\\Dashboard;\nuse Vigilant\\Uptime\\Http\\Livewire\\Tables\\DowntimeTable;\nuse Vigilant\\Uptime\\Http\\Livewire\\Tables\\MonitorTable;\nuse Vigilant\\Uptime\\Http\\Livewire\\UptimeMonitorForm;\nuse Vigilant\\Uptime\\Http\\Livewire\\UptimeMonitors;\nuse Vigilant\\Uptime\\Listeners\\CheckLatencyListener;\nuse Vigilant\\Uptime\\Listeners\\DowntimeEndNotificationListener;\nuse Vigilant\\Uptime\\Listeners\\DowntimeStartNotificationListener;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Notifications\\Conditions\\ClosestCountryCondition;\nuse Vigilant\\Uptime\\Notifications\\Conditions\\CountryCondition;\nuse Vigilant\\Uptime\\Notifications\\Conditions\\LatencyMsCondition;\nuse Vigilant\\Uptime\\Notifications\\Conditions\\LatencyPercentCondition;\nuse Vigilant\\Uptime\\Notifications\\DowntimeEndNotification;\nuse Vigilant\\Uptime\\Notifications\\DowntimeStartNotification;\nuse Vigilant\\Uptime\\Notifications\\LatencyChangedNotification;\nuse Vigilant\\Uptime\\Notifications\\LatencyPeakNotification;\nuse Vigilant\\Users\\Models\\User;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function register(): void\n    {\n        $this\n            ->registerConfig();\n    }\n\n    protected function registerConfig(): static\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/uptime.php', 'uptime');\n\n        return $this;\n    }\n\n    public function boot(): void\n    {\n        $this\n            ->bootConfig()\n            ->bootMigrations()\n            ->bootCommands()\n            ->bootViews()\n            ->bootLivewire()\n            ->bootRoutes()\n            ->bootEvents()\n            ->bootRatelimiting()\n            ->bootNavigation()\n            ->bootNotifications()\n            ->bootGates()\n            ->bootPolicies();\n    }\n\n    protected function bootConfig(): static\n    {\n        $this->publishes([\n            __DIR__.'/../config/uptime.php' => config_path('uptime.php'),\n        ], 'config');\n\n        return $this;\n    }\n\n    protected function bootMigrations(): static\n    {\n        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');\n\n        return $this;\n    }\n\n    protected function bootCommands(): static\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n                CheckUptimeCommand::class,\n                AggregateResultsCommand::class,\n                ScheduleUptimeChecksCommand::class,\n                CheckLatencyCommand::class,\n                GenerateRootCaCommand::class,\n                CheckUnavailableOutpostsCommand::class,\n            ]);\n        }\n\n        return $this;\n    }\n\n    protected function bootViews(): static\n    {\n        $this->loadViewsFrom(__DIR__.'/../resources/views', 'uptime');\n\n        return $this;\n    }\n\n    protected function bootLivewire(): static\n    {\n        Livewire::component('uptime', UptimeMonitors::class);\n        Livewire::component('uptime-monitor-form', UptimeMonitorForm::class);\n\n        Livewire::component('uptime-monitor-table', MonitorTable::class);\n        Livewire::component('uptime-downtime-table', DowntimeTable::class);\n\n        Livewire::component('monitor-column-latency-chart', ColumnLatencyChart::class);\n        Livewire::component('monitor-latency-chart', LatencyChart::class);\n\n        Livewire::component('monitor-dashboard', Dashboard::class);\n\n        return $this;\n    }\n\n    protected function bootRoutes(): static\n    {\n        if (! $this->app->routesAreCached()) {\n            Route::middleware(['web', 'auth'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/web.php'));\n\n            Route::prefix('api')\n                ->middleware(['api'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/api.php'));\n        }\n\n        return $this;\n    }\n\n    protected function bootEvents(): static\n    {\n        Event::listen(DowntimeStartEvent::class, DowntimeStartNotificationListener::class);\n\n        Event::listen(DowntimeEndEvent::class, DowntimeEndNotificationListener::class);\n\n        Event::listen(UptimeCheckedEvent::class, CheckLatencyListener::class);\n\n        return $this;\n    }\n\n    protected function bootRatelimiting(): static\n    {\n        RateLimiter::for('uptime-ips', function (Request $request): Limit {\n            return Limit::perMinute(10)->by($request->ip);\n        });\n\n        return $this;\n    }\n\n    protected function bootNavigation(): static\n    {\n        Navigation::path(__DIR__.'/../resources/navigation.php');\n\n        return $this;\n    }\n\n    protected function bootNotifications(): static\n    {\n        NotificationRegistry::registerNotification([\n            DowntimeStartNotification::class,\n            DowntimeEndNotification::class,\n            LatencyChangedNotification::class,\n            LatencyPeakNotification::class,\n        ]);\n\n        NotificationRegistry::registerCondition(LatencyChangedNotification::class, [\n            LatencyPercentCondition::class,\n            LatencyMsCondition::class,\n            CountryCondition::class,\n            ClosestCountryCondition::class,\n        ]);\n\n        NotificationRegistry::registerCondition(LatencyPeakNotification::class, [\n            LatencyPercentCondition::class,\n            LatencyMsCondition::class,\n            CountryCondition::class,\n            ClosestCountryCondition::class,\n        ]);\n\n        return $this;\n    }\n\n    protected function bootGates(): static\n    {\n        Gate::define('use-uptime', function (User $user) {\n            return ce();\n        });\n\n        return $this;\n    }\n\n    protected function bootPolicies(): static\n    {\n        if (ce()) {\n            Gate::policy(Monitor::class, AllowAllPolicy::class);\n        }\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/uptime/testbench.yaml",
    "content": "providers:\n  - Vigilant\\Uptime\\ServiceProvider\n"
  },
  {
    "path": "packages/uptime/tests/Fakes/HandlerStatsResponse.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests\\Fakes;\n\nuse GuzzleHttp\\Promise\\PromiseInterface;\nuse GuzzleHttp\\Psr7\\Response;\n\nclass HandlerStatsResponse extends Response implements PromiseInterface\n{\n    protected array $handlerStats = [];\n\n    public function withHandlerStats(array $handlerStats): static\n    {\n        $this->handlerStats = $handlerStats;\n\n        return $this;\n    }\n\n    public function handlerStats(): array\n    {\n        return $this->handlerStats;\n    }\n\n    public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface\n    {\n        return $this;\n    }\n\n    public function otherwise(callable $onRejected): PromiseInterface\n    {\n        return $this;\n    }\n\n    public function getState(): string\n    {\n        return '';\n    }\n\n    public function resolve($value): void\n    {\n        //\n    }\n\n    public function reject($reason): void\n    {\n        //\n    }\n\n    public function cancel(): void\n    {\n        //\n    }\n\n    public function wait(bool $unwrap = true)\n    {\n        //\n    }\n}\n"
  },
  {
    "path": "packages/uptime/tests/Feature/AggregateResultsTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests\\Feature;\n\nuse Vigilant\\Uptime\\Commands\\AggregateResultsCommand;\nuse Vigilant\\Uptime\\Enums\\Type;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Tests\\TestCase;\n\nclass AggregateResultsTest extends TestCase\n{\n    public function test_it_aggregates_uptime_results(): void\n    {\n        $monitor = null;\n\n        Monitor::withoutEvents(function () use (&$monitor) {\n            /** @var Monitor $monitor */\n            $monitor = Monitor::query()->create([\n                'team_id' => 1,\n                'name' => 'Test Monitor',\n                'type' => Type::Http,\n                'settings' => [\n                    'host' => 'http://service',\n                ],\n                'interval' => '* * * * *',\n                'retries' => 1,\n                'timeout' => 1,\n            ]);\n        });\n\n        $this->assertNotNull($monitor);\n\n        for ($minute = 0; $minute < (60 * 24); $minute++) {\n\n            $date = now()->subMinutes($minute);\n\n            $monitor->results()->create([\n                'total_time' => $minute,\n                'country' => 'US',\n                'created_at' => $date,\n                'updated_at' => $date,\n            ]);\n\n        }\n\n        $this->artisan(AggregateResultsCommand::class);\n\n        $this->assertCount(24, $monitor->aggregatedResults);\n    }\n\n    public function test_it_aggregates_uptime_results_per_country(): void\n    {\n        $monitor = null;\n\n        Monitor::withoutEvents(function () use (&$monitor) {\n            /** @var Monitor $monitor */\n            $monitor = Monitor::query()->create([\n                'team_id' => 1,\n                'name' => 'Test Monitor',\n                'type' => Type::Http,\n                'settings' => [\n                    'host' => 'http://service',\n                ],\n                'interval' => '* * * * *',\n                'retries' => 1,\n                'timeout' => 1,\n            ]);\n        });\n\n        $this->assertNotNull($monitor);\n\n        // Create results from different countries\n        for ($minute = 0; $minute < 120; $minute++) {\n            $date = now()->subMinutes($minute);\n            $country = $minute % 2 === 0 ? 'US' : 'DE';\n\n            $monitor->results()->create([\n                'total_time' => $minute,\n                'country' => $country,\n                'created_at' => $date,\n                'updated_at' => $date,\n            ]);\n        }\n\n        $this->artisan(AggregateResultsCommand::class);\n\n        // Should create separate aggregates for US and DE\n        $usAggregates = $monitor->aggregatedResults()->where('country', 'US')->count();\n        $deAggregates = $monitor->aggregatedResults()->where('country', 'DE')->count();\n\n        $this->assertGreaterThan(0, $usAggregates);\n        $this->assertGreaterThan(0, $deAggregates);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/tests/Feature/DowntimeTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests\\Feature;\n\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Uptime\\Commands\\CheckUptimeCommand;\nuse Vigilant\\Uptime\\Enums\\Type;\nuse Vigilant\\Uptime\\Events\\DowntimeEndEvent;\nuse Vigilant\\Uptime\\Events\\DowntimeStartEvent;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Tests\\TestCase;\n\nclass DowntimeTest extends TestCase\n{\n    public function test_it_checks_dispatches_event_on_downtime_once(): void\n    {\n        Event::fake();\n\n        $monitor = null;\n\n        Monitor::withoutEvents(function () use (&$monitor) {\n            /** @var Monitor $monitor */\n            $monitor = Monitor::query()->create([\n                'team_id' => 1,\n                'name' => 'Test Monitor',\n                'type' => Type::Http,\n                'settings' => [\n                    'host' => 'http://service',\n                ],\n                'interval' => '* * * * *',\n                'retries' => 0,\n                'timeout' => 1,\n            ]);\n        });\n\n        $this->assertNotNull($monitor);\n\n        $outpost = \\Vigilant\\Uptime\\Models\\Outpost::create([\n            'ip' => '127.0.0.1',\n            'port' => 3000,\n            'external_ip' => '127.0.0.1',\n            'status' => \\Vigilant\\Uptime\\Enums\\OutpostStatus::Available,\n            'country' => 'US',\n            'last_available_at' => now(),\n        ]);\n\n        Http::fake([\n            'https://127.0.0.1:3000/*' => Http::response([\n                'up' => false,\n                'latency_ms' => 0,\n            ]),\n        ]);\n\n        $this->artisan(CheckUptimeCommand::class, [\n            'monitorId' => $monitor->id,\n        ]);\n\n        $this->artisan(CheckUptimeCommand::class, [\n            'monitorId' => $monitor->id,\n        ]);\n\n        Event::assertDispatchedTimes(DowntimeStartEvent::class, 1);\n    }\n\n    public function test_it_resolves_downtime(): void\n    {\n        Event::fake();\n\n        $monitor = null;\n\n        Monitor::withoutEvents(function () use (&$monitor) {\n            /** @var Monitor $monitor */\n            $monitor = Monitor::query()->create([\n                'team_id' => 1,\n                'name' => 'Test Monitor',\n                'type' => Type::Http,\n                'settings' => [\n                    'host' => 'http://service',\n                ],\n                'interval' => '* * * * *',\n                'retries' => 0,\n                'timeout' => 1,\n            ]);\n        });\n\n        $this->assertNotNull($monitor);\n\n        $monitor->downtimes()->create([\n            'start' => now()->subMinutes(5),\n        ]);\n\n        $outpost = \\Vigilant\\Uptime\\Models\\Outpost::create([\n            'ip' => '127.0.0.1',\n            'port' => 3000,\n            'external_ip' => '127.0.0.1',\n            'status' => \\Vigilant\\Uptime\\Enums\\OutpostStatus::Available,\n            'country' => 'US',\n            'last_available_at' => now(),\n        ]);\n\n        Http::fake([\n            'https://127.0.0.1:3000/*' => Http::response([\n                'up' => true,\n                'latency_ms' => 100,\n            ]),\n        ]);\n\n        $this->artisan(CheckUptimeCommand::class, [\n            'monitorId' => $monitor->id,\n        ]);\n\n        Event::assertDispatched(DowntimeEndEvent::class);\n\n        $this->assertNotNull($monitor->downtimes()->whereNotNull('end')->first());\n    }\n}\n"
  },
  {
    "path": "packages/uptime/tests/Feature/UptimeTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests\\Feature;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Uptime\\Commands\\CheckUptimeCommand;\nuse Vigilant\\Uptime\\Enums\\Type;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Models\\Result;\nuse Vigilant\\Uptime\\Tests\\TestCase;\n\nclass UptimeTest extends TestCase\n{\n    public function test_it_checks_uptime_via_http(): void\n    {\n        $monitor = null;\n\n        /** @var Monitor $monitor */\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => [\n                'host' => 'http://service',\n            ],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 1,\n        ]);\n\n        $outpost = \\Vigilant\\Uptime\\Models\\Outpost::create([\n            'ip' => '127.0.0.1',\n            'port' => 3000,\n            'external_ip' => '127.0.0.1',\n            'status' => \\Vigilant\\Uptime\\Enums\\OutpostStatus::Available,\n            'country' => 'US',\n            'last_available_at' => now(),\n        ]);\n\n        Http::fake([\n            'https://127.0.0.1:3000/*' => Http::response([\n                'up' => true,\n                'latency_ms' => 100,\n            ]),\n        ]);\n\n        $this->artisan(CheckUptimeCommand::class, [\n            'monitorId' => $monitor->id,\n        ]);\n\n        $this->artisan(CheckUptimeCommand::class, [\n            'monitorId' => $monitor->id,\n        ]);\n\n        $results = $monitor->results;\n\n        $this->assertCount(2, $results);\n    }\n\n    public function test_it_checks_uptime_via_ping(): void\n    {\n        $monitor = null;\n\n        /** @var Monitor $monitor */\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Ping,\n            'settings' => [\n                'host' => '127.0.0.1',\n            ],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 1,\n        ]);\n\n        $outpost = \\Vigilant\\Uptime\\Models\\Outpost::create([\n            'ip' => '127.0.0.1',\n            'port' => 3000,\n            'external_ip' => '127.0.0.1',\n            'status' => \\Vigilant\\Uptime\\Enums\\OutpostStatus::Available,\n            'country' => 'US',\n            'last_available_at' => now(),\n        ]);\n\n        Http::fake([\n            'https://127.0.0.1:3000/*' => Http::response([\n                'up' => true,\n                'latency_ms' => 10,\n            ]),\n        ]);\n\n        $this->artisan(CheckUptimeCommand::class, [\n            'monitorId' => $monitor->id,\n        ]);\n        /** @var Result $result */\n        $result = $monitor->results->first();\n\n        $this->assertEquals(10, $result->total_time);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Livewire\\LivewireServiceProvider;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Uptime\\ServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n            \\Vigilant\\Core\\ServiceProvider::class,\n            \\Vigilant\\Users\\ServiceProvider::class,\n            LivewireServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n    }\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        TeamService::fake();\n    }\n}\n"
  },
  {
    "path": "packages/uptime/tests/Unit/CalculateUptimePercentageTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests\\Unit;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Carbon;\nuse Vigilant\\Uptime\\Actions\\CalculateUptimePercentage;\nuse Vigilant\\Uptime\\Models\\Downtime;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Models\\ResultAggregate;\nuse Vigilant\\Uptime\\Tests\\TestCase;\n\nclass CalculateUptimePercentageTest extends TestCase\n{\n    public function test_it_calculates_uptime(): void\n    {\n        Carbon::setTestNow(Carbon::parse('2024-02-24 10:00:00'));\n\n        /** @var Monitor $monitor */\n        $monitor = Monitor::withoutEvents(fn (): Model => Monitor::factory()->create());\n\n        ResultAggregate::query()->create([\n            'monitor_id' => $monitor->id,\n            'total_time' => 0,\n            'created_at' => '2024-02-24 00:00:00',\n        ]);\n\n        Downtime::query()->create([\n            'monitor_id' => $monitor->id,\n            'start' => '2024-02-24 00:00:00',\n            'end' => '2024-02-24 01:00:00',\n            'created_at' => '2024-02-24 00:00:00',\n            'updated_at' => '2024-02-24 01:00:00',\n        ]);\n\n        /** @var CalculateUptimePercentage $action */\n        $action = app(CalculateUptimePercentage::class);\n\n        $this->assertEquals(90, $action->calculate($monitor));\n\n    }\n}\n"
  },
  {
    "path": "packages/uptime/tests/Unit/DetermineOutpostPerformanceTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests\\Unit;\n\nuse Vigilant\\Uptime\\Actions\\Outpost\\DetermineOutpost;\nuse Vigilant\\Uptime\\Enums\\OutpostStatus;\nuse Vigilant\\Uptime\\Enums\\Type;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Models\\Outpost;\nuse Vigilant\\Uptime\\Tests\\TestCase;\n\nclass DetermineOutpostPerformanceTest extends TestCase\n{\n    public function test_it_efficiently_handles_thousands_of_outposts(): void\n    {\n        // Create a monitor\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => ['host' => 'http://example.com'],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n            'country' => 'US',\n            'latitude' => 40.0,\n            'longitude' => -70.0,\n        ]);\n\n        // Create thousands of outposts efficiently\n        $outpostsData = [];\n        $countries = ['US', 'UK', 'DE', 'FR', 'JP', 'AU', 'CA', 'BR', 'IN', 'SG'];\n\n        for ($i = 0; $i < 1000; $i++) {\n            $country = $countries[$i % count($countries)];\n            $outpostsData[] = [\n                'ip' => '192.168.'.floor($i / 256).'.'.($i % 256),\n                'port' => 8080 + ($i % 100),\n                'external_ip' => floor($i / 256).'.'.($i % 256).'.1.1',\n                'status' => OutpostStatus::Available->value,\n                'country' => $country,\n                'latitude' => 40.0 + ($i % 10),\n                'longitude' => -70.0 + ($i % 10),\n                'last_available_at' => now(),\n                'created_at' => now(),\n                'updated_at' => now(),\n            ];\n        }\n\n        // Batch insert for performance\n        foreach (array_chunk($outpostsData, 500) as $chunk) {\n            Outpost::query()->insert($chunk);\n        }\n\n        $determineOutpost = new DetermineOutpost;\n\n        // Measure performance\n        $startTime = microtime(true);\n\n        $iterations = 100;\n        for ($i = 0; $i < $iterations; $i++) {\n            $outpost = $determineOutpost->determine($monitor);\n            $this->assertNotNull($outpost);\n        }\n\n        $endTime = microtime(true);\n        $avgTime = ($endTime - $startTime) / $iterations;\n\n        // Should complete each selection in less than 10ms on average\n        $this->assertLessThan(0.01, $avgTime, 'Average selection time should be less than 10ms');\n\n        // Verify distribution - with distance-based selection, we should have a mix\n        // of closest and remote outposts\n        $closestSelections = 0;\n        $remoteSelections = 0;\n\n        for ($i = 0; $i < 100; $i++) {\n            $outpost = $determineOutpost->determine($monitor);\n            $this->assertNotNull($outpost);\n            // The closest outpost will be at 40.0, -70.0 (the first one created)\n            if ($outpost->latitude == 40.0 && $outpost->longitude == -70.0) {\n                $closestSelections++;\n            } else {\n                $remoteSelections++;\n            }\n        }\n\n        // Should maintain roughly 50/50 distribution between closest and remote\n        $this->assertGreaterThan(30, $closestSelections);\n        $this->assertGreaterThan(30, $remoteSelections);\n    }\n\n    public function test_it_uses_database_queries_not_memory(): void\n    {\n        // Create monitor and outposts\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => ['host' => 'http://example.com'],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n            'country' => 'US',\n            'latitude' => 40.0,\n            'longitude' => -70.0,\n        ]);\n\n        // Create 100 outposts\n        for ($i = 0; $i < 100; $i++) {\n            Outpost::query()->create([\n                'ip' => \"192.168.1.{$i}\",\n                'port' => 8080,\n                'external_ip' => \"1.2.3.{$i}\",\n                'status' => OutpostStatus::Available,\n                'country' => $i < 50 ? 'US' : 'UK',\n                'latitude' => 40.0 + $i,\n                'longitude' => -70.0 + $i,\n                'last_available_at' => now(),\n            ]);\n        }\n\n        // Enable query log\n        \\DB::enableQueryLog();\n\n        $determineOutpost = new DetermineOutpost;\n        $determineOutpost->determine($monitor);\n\n        $queries = \\DB::getQueryLog();\n\n        // Should use efficient queries\n        // - 1 query to find/update closest outpost\n        // - 1 query to select either closest or remote outpost\n        // - 1 update query for monitor's closest_outpost_id\n        $this->assertLessThanOrEqual(3, count($queries), 'Should use at most 3 queries');\n\n        foreach ($queries as $query) {\n            // Verify queries use LIMIT to avoid loading all records\n            $sql = strtolower($query['query']);\n\n            // All select queries should have a limit\n            if (strpos($sql, 'select') !== false && strpos($sql, 'from') !== false) {\n                $this->assertTrue(\n                    strpos($sql, 'limit') !== false,\n                    'Query should use LIMIT to avoid loading all records: '.$query['query']\n                );\n            }\n        }\n    }\n\n    public function test_remote_selection_distributes_across_all_countries(): void\n    {\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => ['host' => 'http://example.com'],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n            'country' => 'US',\n            'latitude' => 40.0,\n            'longitude' => -70.0,\n        ]);\n\n        // Create outposts in different countries\n        $countries = ['UK', 'DE', 'FR', 'JP', 'AU'];\n        foreach ($countries as $index => $country) {\n            Outpost::query()->create([\n                'ip' => \"192.168.1.{$index}\",\n                'port' => 8080,\n                'external_ip' => \"1.2.3.{$index}\",\n                'status' => OutpostStatus::Available,\n                'country' => $country,\n                'latitude' => 40.0 + ($index * 5),\n                'longitude' => -70.0 + ($index * 5),\n                'last_available_at' => now(),\n            ]);\n        }\n\n        $determineOutpost = new DetermineOutpost;\n        $selectedCountries = [];\n\n        // Run many selections to see distribution\n        for ($i = 0; $i < 100; $i++) {\n            $outpost = $determineOutpost->determine($monitor);\n            $this->assertNotNull($outpost);\n            if ($outpost->country !== 'US') {\n                $selectedCountries[] = $outpost->country;\n            }\n        }\n\n        // Should have selected from multiple different countries\n        $uniqueCountries = array_unique($selectedCountries);\n        $this->assertGreaterThan(1, count($uniqueCountries), 'Should select from multiple remote countries');\n    }\n}\n"
  },
  {
    "path": "packages/uptime/tests/Unit/DetermineOutpostTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests\\Unit;\n\nuse Vigilant\\Uptime\\Actions\\Outpost\\DetermineOutpost;\nuse Vigilant\\Uptime\\Enums\\OutpostStatus;\nuse Vigilant\\Uptime\\Enums\\Type;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Models\\Outpost;\nuse Vigilant\\Uptime\\Tests\\TestCase;\n\nclass DetermineOutpostTest extends TestCase\n{\n    public function test_it_returns_null_when_no_outposts_available(): void\n    {\n        $determineOutpost = new DetermineOutpost;\n\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => ['host' => 'http://example.com'],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n        ]);\n\n        $result = $determineOutpost->determine($monitor);\n\n        $this->assertNull($result);\n    }\n\n    public function test_it_selects_same_country_outpost_approximately_50_percent(): void\n    {\n        // Create a monitor in Boston, US\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => ['host' => 'http://example.com'],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589,\n        ]);\n\n        // Create outposts: two in US (Boston and Chicago), one in UK (London), one in DE (Berlin)\n        $usOutpost1 = Outpost::query()->create([\n            'ip' => '192.168.1.1',\n            'port' => 8080,\n            'external_ip' => '1.2.3.4',\n            'status' => OutpostStatus::Available,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589, // Boston (same as monitor - closest)\n            'last_available_at' => now(),\n        ]);\n\n        $usOutpost2 = Outpost::query()->create([\n            'ip' => '192.168.1.2',\n            'port' => 8080,\n            'external_ip' => '1.2.3.5',\n            'status' => OutpostStatus::Available,\n            'country' => 'US',\n            'latitude' => 41.8781,\n            'longitude' => -87.6298, // Chicago\n            'last_available_at' => now(),\n        ]);\n\n        $ukOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.3',\n            'port' => 8080,\n            'external_ip' => '1.2.3.6',\n            'status' => OutpostStatus::Available,\n            'country' => 'UK',\n            'latitude' => 51.5074,\n            'longitude' => -0.1278, // London\n            'last_available_at' => now(),\n        ]);\n\n        $deOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.4',\n            'port' => 8081,\n            'external_ip' => '1.2.3.7',\n            'status' => OutpostStatus::Available,\n            'country' => 'DE',\n            'latitude' => 52.5200,\n            'longitude' => 13.4050, // Berlin\n            'last_available_at' => now(),\n        ]);\n\n        $determineOutpost = new DetermineOutpost;\n\n        $closestCount = 0;\n        $remoteCount = 0;\n\n        // Run the selection 200 times for better statistical distribution\n        for ($i = 0; $i < 200; $i++) {\n            $selected = $determineOutpost->determine($monitor);\n            $this->assertNotNull($selected);\n\n            if ($selected->id === $usOutpost1->id) {\n                $closestCount++;\n            } else {\n                $remoteCount++;\n            }\n        }\n\n        // Closest outpost (Boston) should be selected approximately 50% of the time (allow variance for randomness)\n        $this->assertGreaterThan(60, $closestCount);\n        $this->assertLessThan(140, $closestCount);\n\n        // Remote outposts should be selected approximately 50% of the time\n        $this->assertGreaterThan(60, $remoteCount);\n        $this->assertLessThan(140, $remoteCount);\n    }\n\n    public function test_it_distributes_remote_country_outposts_evenly(): void\n    {\n        // Create monitors with different IDs and locations to test distribution\n        $monitors = [];\n        for ($i = 0; $i < 10; $i++) {\n            $monitors[] = Monitor::query()->create([\n                'team_id' => 1,\n                'name' => \"Test Monitor {$i}\",\n                'type' => Type::Http,\n                'settings' => ['host' => 'http://example.com'],\n                'interval' => '* * * * *',\n                'retries' => 1,\n                'timeout' => 5,\n                'country' => 'US',\n                'latitude' => 42.3601,\n                'longitude' => -71.0589, // Boston\n            ]);\n        }\n\n        // Create outposts\n        $usOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.1',\n            'port' => 8080,\n            'external_ip' => '1.2.3.4',\n            'status' => OutpostStatus::Available,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589, // Boston (closest)\n            'last_available_at' => now(),\n        ]);\n\n        $ukOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.2',\n            'port' => 8080,\n            'external_ip' => '1.2.3.5',\n            'status' => OutpostStatus::Available,\n            'country' => 'UK',\n            'latitude' => 51.5074,\n            'longitude' => -0.1278, // London (farthest from Boston)\n            'last_available_at' => now(),\n        ]);\n\n        $deOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.3',\n            'port' => 8080,\n            'external_ip' => '1.2.3.6',\n            'status' => OutpostStatus::Available,\n            'country' => 'DE',\n            'latitude' => 52.5200,\n            'longitude' => 13.4050, // Berlin\n            'last_available_at' => now(),\n        ]);\n\n        $determineOutpost = new DetermineOutpost;\n\n        $ukSelections = 0;\n        $deSelections = 0;\n\n        // Test remote selection for multiple runs per monitor\n        foreach ($monitors as $monitor) {\n            // Run selection multiple times, filter for remote selections only\n            for ($i = 0; $i < 20; $i++) {\n                $selected = $determineOutpost->determine($monitor);\n                $this->assertNotNull($selected);\n\n                if ($selected->id !== $usOutpost->id) {\n                    if ($selected->country === 'UK') {\n                        $ukSelections++;\n                    } elseif ($selected->country === 'DE') {\n                        $deSelections++;\n                    }\n                }\n            }\n        }\n\n        // Both remote outposts should have been selected\n        $this->assertGreaterThan(0, $ukSelections);\n        $this->assertGreaterThan(0, $deSelections);\n    }\n\n    public function test_it_handles_monitor_without_country(): void\n    {\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => ['host' => 'http://example.com'],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n        ]);\n\n        $outpost = Outpost::query()->create([\n            'ip' => '192.168.1.1',\n            'port' => 8080,\n            'external_ip' => '1.2.3.4',\n            'status' => OutpostStatus::Available,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589,\n            'last_available_at' => now(),\n        ]);\n\n        $determineOutpost = new DetermineOutpost;\n\n        $result = $determineOutpost->determine($monitor);\n\n        $this->assertNotNull($result);\n        $this->assertEquals($outpost->id, $result->id);\n    }\n\n    public function test_it_handles_single_outpost_in_same_country(): void\n    {\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => ['host' => 'http://example.com'],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589,\n        ]);\n\n        $outpost = Outpost::query()->create([\n            'ip' => '192.168.1.1',\n            'port' => 8080,\n            'external_ip' => '1.2.3.4',\n            'status' => OutpostStatus::Available,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589,\n            'last_available_at' => now(),\n        ]);\n\n        $determineOutpost = new DetermineOutpost;\n\n        // Should always return the single outpost\n        for ($i = 0; $i < 10; $i++) {\n            $result = $determineOutpost->determine($monitor);\n            $this->assertNotNull($result);\n            $this->assertEquals($outpost->id, $result->id);\n        }\n    }\n\n    public function test_it_handles_no_same_country_outposts(): void\n    {\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => ['host' => 'http://example.com'],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589,\n        ]);\n\n        // Only create outposts in other countries\n        $ukOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.1',\n            'port' => 8080,\n            'external_ip' => '1.2.3.4',\n            'status' => OutpostStatus::Available,\n            'country' => 'UK',\n            'latitude' => 51.5074,\n            'longitude' => -0.1278,\n            'last_available_at' => now(),\n        ]);\n\n        $deOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.2',\n            'port' => 8080,\n            'external_ip' => '1.2.3.5',\n            'status' => OutpostStatus::Available,\n            'country' => 'DE',\n            'latitude' => 52.5200,\n            'longitude' => 13.4050,\n            'last_available_at' => now(),\n        ]);\n\n        $determineOutpost = new DetermineOutpost;\n\n        // Should still return an outpost (from other countries)\n        $result = $determineOutpost->determine($monitor);\n\n        $this->assertNotNull($result);\n        $this->assertContains($result->country, ['UK', 'DE']);\n    }\n\n    public function test_it_stores_closest_outpost_id(): void\n    {\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => ['host' => 'http://example.com'],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589,\n        ]);\n\n        $closestOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.1',\n            'port' => 8080,\n            'external_ip' => '1.2.3.4',\n            'status' => OutpostStatus::Available,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589, // Same location as monitor\n            'last_available_at' => now(),\n        ]);\n\n        $farOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.2',\n            'port' => 8080,\n            'external_ip' => '1.2.3.5',\n            'status' => OutpostStatus::Available,\n            'country' => 'UK',\n            'latitude' => 51.5074,\n            'longitude' => -0.1278,\n            'last_available_at' => now(),\n        ]);\n\n        $determineOutpost = new DetermineOutpost;\n\n        // Initially, no closest outpost is set\n        $this->assertNull($monitor->closest_outpost_id);\n\n        // Determine outpost should store the closest one\n        $result = $determineOutpost->determine($monitor);\n\n        $monitor->refresh();\n        $this->assertNotNull($monitor->closest_outpost_id);\n        $this->assertEquals($closestOutpost->id, $monitor->closest_outpost_id);\n    }\n\n    public function test_it_nullifies_closest_outpost_when_outpost_deleted(): void\n    {\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => ['host' => 'http://example.com'],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589,\n        ]);\n\n        $outpost = Outpost::query()->create([\n            'ip' => '192.168.1.1',\n            'port' => 8080,\n            'external_ip' => '1.2.3.4',\n            'status' => OutpostStatus::Available,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589,\n            'last_available_at' => now(),\n        ]);\n\n        // Set the closest outpost\n        $monitor->update(['closest_outpost_id' => $outpost->id]);\n\n        // Delete the outpost\n        $outpost->delete();\n\n        // Verify the closest_outpost_id is set to null\n        $monitor->refresh();\n        $this->assertNull($monitor->closest_outpost_id);\n    }\n\n    public function test_it_nullifies_closest_outpost_when_outpost_becomes_unavailable(): void\n    {\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => ['host' => 'http://example.com'],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589,\n        ]);\n\n        $outpost = Outpost::query()->create([\n            'ip' => '192.168.1.1',\n            'port' => 8080,\n            'external_ip' => '1.2.3.4',\n            'status' => OutpostStatus::Available,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589,\n            'last_available_at' => now(),\n        ]);\n\n        // Set the closest outpost\n        $monitor->update(['closest_outpost_id' => $outpost->id]);\n\n        // Mark the outpost as unavailable\n        $outpost->update(['status' => OutpostStatus::Unavailable]);\n\n        // Verify the closest_outpost_id is set to null\n        $monitor->refresh();\n        $this->assertNull($monitor->closest_outpost_id);\n    }\n\n    public function test_it_uses_cached_closest_outpost_when_available(): void\n    {\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => ['host' => 'http://example.com'],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589,\n        ]);\n\n        $closestOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.1',\n            'port' => 8080,\n            'external_ip' => '1.2.3.4',\n            'status' => OutpostStatus::Available,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589,\n            'last_available_at' => now(),\n        ]);\n\n        $remoteOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.2',\n            'port' => 8080,\n            'external_ip' => '1.2.3.5',\n            'status' => OutpostStatus::Available,\n            'country' => 'UK',\n            'latitude' => 51.5074,\n            'longitude' => -0.1278,\n            'last_available_at' => now(),\n        ]);\n\n        // Pre-set the closest outpost\n        $monitor->update(['closest_outpost_id' => $closestOutpost->id]);\n\n        $determineOutpost = new DetermineOutpost;\n\n        // When selecting closest, it should use the cached value\n        $closestSelections = 0;\n        for ($i = 0; $i < 100; $i++) {\n            $result = $determineOutpost->determine($monitor);\n            if ($result !== null && $result->id === $closestOutpost->id) {\n                $closestSelections++;\n            }\n        }\n\n        // Should use the cached closest outpost approximately 50% of the time\n        $this->assertGreaterThan(30, $closestSelections);\n        $this->assertLessThan(70, $closestSelections);\n    }\n\n    public function test_excluded_outposts_dont_affect_closest_cache(): void\n    {\n        $monitor = Monitor::query()->create([\n            'team_id' => 1,\n            'name' => 'Test Monitor',\n            'type' => Type::Http,\n            'settings' => ['host' => 'http://example.com'],\n            'interval' => '* * * * *',\n            'retries' => 1,\n            'timeout' => 5,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589,\n        ]);\n\n        $closestOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.1',\n            'port' => 8080,\n            'external_ip' => '1.2.3.4',\n            'status' => OutpostStatus::Available,\n            'country' => 'US',\n            'latitude' => 42.3601,\n            'longitude' => -71.0589, // Same as monitor (closest)\n            'last_available_at' => now(),\n        ]);\n\n        $secondClosest = Outpost::query()->create([\n            'ip' => '192.168.1.2',\n            'port' => 8080,\n            'external_ip' => '1.2.3.5',\n            'status' => OutpostStatus::Available,\n            'country' => 'US',\n            'latitude' => 42.3650,\n            'longitude' => -71.0600, // Very close to monitor\n            'last_available_at' => now(),\n        ]);\n\n        $remoteOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.3',\n            'port' => 8080,\n            'external_ip' => '1.2.3.6',\n            'status' => OutpostStatus::Available,\n            'country' => 'UK',\n            'latitude' => 51.5074,\n            'longitude' => -0.1278,\n            'last_available_at' => now(),\n        ]);\n\n        $determineOutpost = new DetermineOutpost;\n\n        // First call should set the closest outpost\n        $determineOutpost->determine($monitor);\n        $monitor->refresh();\n        $this->assertEquals($closestOutpost->id, $monitor->closest_outpost_id);\n\n        // When we exclude the closest outpost (simulating retry after failure),\n        // it should NOT return the excluded outpost\n        // Test multiple times to account for randomness in selection\n        $excludedReturned = false;\n        for ($i = 0; $i < 10; $i++) {\n            $result = $determineOutpost->determine($monitor, [$closestOutpost->id]);\n\n            // Should never get the excluded closest outpost\n            if ($result !== null && $result->id === $closestOutpost->id) {\n                $excludedReturned = true;\n                break;\n            }\n        }\n\n        $this->assertFalse($excludedReturned, 'Excluded outpost should never be returned');\n\n        // The cached closest_outpost_id should NOT have changed\n        $monitor->refresh();\n        $this->assertEquals($closestOutpost->id, $monitor->closest_outpost_id,\n            'Closest outpost cache should not change when using excluded outposts');\n    }\n}\n"
  },
  {
    "path": "packages/uptime/tests/Unit/Enums/TypeTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests\\Unit\\Enums;\n\nuse Vigilant\\Uptime\\Enums\\Type;\nuse Vigilant\\Uptime\\Models\\Monitor;\nuse Vigilant\\Uptime\\Tests\\TestCase;\n\nclass TypeTest extends TestCase\n{\n    public function test_http_format_target_accepts_ip_host(): void\n    {\n        /** @var Monitor $monitor */\n        $monitor = Monitor::factory()->make([\n            'settings' => ['host' => '127.0.0.1'],\n            'type' => Type::Http,\n        ]);\n\n        $this->assertSame('127.0.0.1', Type::Http->formatTarget($monitor));\n    }\n}\n"
  },
  {
    "path": "packages/uptime/tests/Unit/ExternalOutpostMiddlewareTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests\\Unit;\n\nuse Illuminate\\Http\\Request;\nuse Vigilant\\Uptime\\Http\\Middleware\\ExternalOutpostMiddleware;\nuse Vigilant\\Uptime\\Tests\\TestCase;\n\nclass ExternalOutpostMiddlewareTest extends TestCase\n{\n    public function test_it_allows_private_ip_when_external_outposts_disabled(): void\n    {\n        config(['uptime.allow_external_outposts' => false]);\n\n        $middleware = new ExternalOutpostMiddleware;\n        $request = Request::create('/test', 'GET');\n        $request->server->set('REMOTE_ADDR', '192.168.1.1');\n\n        $response = $middleware->handle($request, function ($req) {\n            return response()->json(['success' => true]);\n        });\n\n        $this->assertEquals(200, $response->getStatusCode());\n        $this->assertEquals(['success' => true], $response->getData(true));\n    }\n\n    public function test_it_allows_localhost_when_external_outposts_disabled(): void\n    {\n        config(['uptime.allow_external_outposts' => false]);\n\n        $middleware = new ExternalOutpostMiddleware;\n        $request = Request::create('/test', 'GET');\n        $request->server->set('REMOTE_ADDR', '127.0.0.1');\n\n        $response = $middleware->handle($request, function ($req) {\n            return response()->json(['success' => true]);\n        });\n\n        $this->assertEquals(200, $response->getStatusCode());\n        $this->assertEquals(['success' => true], $response->getData(true));\n    }\n\n    public function test_it_denies_public_ip_when_external_outposts_disabled(): void\n    {\n        config(['uptime.allow_external_outposts' => false]);\n\n        $middleware = new ExternalOutpostMiddleware;\n        $request = Request::create('/test', 'GET');\n        $request->server->set('REMOTE_ADDR', '8.8.8.8');\n\n        $response = $middleware->handle($request, function ($req) {\n            return response()->json(['success' => true]);\n        });\n\n        $this->assertEquals(403, $response->getStatusCode());\n        $this->assertEquals(['message' => 'External outposts are not allowed.'], $response->getData(true));\n    }\n\n    public function test_it_allows_public_ip_when_external_outposts_enabled(): void\n    {\n        config(['uptime.allow_external_outposts' => true]);\n\n        $middleware = new ExternalOutpostMiddleware;\n        $request = Request::create('/test', 'GET');\n        $request->server->set('REMOTE_ADDR', '8.8.8.8');\n\n        $response = $middleware->handle($request, function ($req) {\n            return response()->json(['success' => true]);\n        });\n\n        $this->assertEquals(200, $response->getStatusCode());\n        $this->assertEquals(['success' => true], $response->getData(true));\n    }\n\n    public function test_it_allows_private_ip_when_external_outposts_enabled(): void\n    {\n        config(['uptime.allow_external_outposts' => true]);\n\n        $middleware = new ExternalOutpostMiddleware;\n        $request = Request::create('/test', 'GET');\n        $request->server->set('REMOTE_ADDR', '10.0.0.1');\n\n        $response = $middleware->handle($request, function ($req) {\n            return response()->json(['success' => true]);\n        });\n\n        $this->assertEquals(200, $response->getStatusCode());\n        $this->assertEquals(['success' => true], $response->getData(true));\n    }\n\n    public function test_it_allows_reserved_ip_when_external_outposts_disabled(): void\n    {\n        config(['uptime.allow_external_outposts' => false]);\n\n        $middleware = new ExternalOutpostMiddleware;\n        $request = Request::create('/test', 'GET');\n        $request->server->set('REMOTE_ADDR', '169.254.1.1');\n\n        $response = $middleware->handle($request, function ($req) {\n            return response()->json(['success' => true]);\n        });\n\n        $this->assertEquals(200, $response->getStatusCode());\n        $this->assertEquals(['success' => true], $response->getData(true));\n    }\n\n    public function test_it_allows_ipv6_private_address(): void\n    {\n        config(['uptime.allow_external_outposts' => false]);\n\n        $middleware = new ExternalOutpostMiddleware;\n        $request = Request::create('/test', 'GET');\n        $request->server->set('REMOTE_ADDR', 'fd00::1');\n\n        $response = $middleware->handle($request, function ($req) {\n            return response()->json(['success' => true]);\n        });\n\n        $this->assertEquals(200, $response->getStatusCode());\n        $this->assertEquals(['success' => true], $response->getData(true));\n    }\n\n    public function test_it_denies_ipv6_public_address_when_external_outposts_disabled(): void\n    {\n        config(['uptime.allow_external_outposts' => false]);\n\n        $middleware = new ExternalOutpostMiddleware;\n        $request = Request::create('/test', 'GET');\n        $request->server->set('REMOTE_ADDR', '2001:4860:4860::8888');\n\n        $response = $middleware->handle($request, function ($req) {\n            return response()->json(['success' => true]);\n        });\n\n        $this->assertEquals(403, $response->getStatusCode());\n        $this->assertEquals(['message' => 'External outposts are not allowed.'], $response->getData(true));\n    }\n\n    public function test_it_allows_ipv6_public_address_when_external_outposts_enabled(): void\n    {\n        config(['uptime.allow_external_outposts' => true]);\n\n        $middleware = new ExternalOutpostMiddleware;\n        $request = Request::create('/test', 'GET');\n        $request->server->set('REMOTE_ADDR', '2001:4860:4860::8888');\n\n        $response = $middleware->handle($request, function ($req) {\n            return response()->json(['success' => true]);\n        });\n\n        $this->assertEquals(200, $response->getStatusCode());\n        $this->assertEquals(['success' => true], $response->getData(true));\n    }\n}\n"
  },
  {
    "path": "packages/uptime/tests/Unit/FetchGeolocationTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests\\Unit;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Mockery\\MockInterface;\nuse Vigilant\\Dns\\Actions\\ResolveRecord;\nuse Vigilant\\Uptime\\Actions\\FetchGeolocation;\nuse Vigilant\\Uptime\\Tests\\TestCase;\n\nclass FetchGeolocationTest extends TestCase\n{\n    public function test_it_fetches_geolocation_for_hostname(): void\n    {\n        $this->mock(ResolveRecord::class, function (MockInterface $mock) {\n            $mock->shouldReceive('resolve')->andReturn('93.184.216.34');\n        });\n\n        Http::fake([\n            'https://free.freeipapi.com/api/json/*' => Http::response([\n                'countryCode' => 'US',\n                'latitude' => 40.7128,\n                'longitude' => -74.0060,\n            ]),\n        ]);\n\n        $fetchGeolocation = app(FetchGeolocation::class);\n\n        $result = $fetchGeolocation->fetch('example.com');\n\n        $this->assertNotNull($result);\n        $this->assertEquals('US', $result['country']);\n        $this->assertEquals(40.7128, $result['latitude']);\n        $this->assertEquals(-74.0060, $result['longitude']);\n    }\n\n    public function test_it_extracts_hostname_from_url(): void\n    {\n        $this->mock(ResolveRecord::class, function (MockInterface $mock) {\n            $mock->shouldReceive('resolve')->andReturn('93.184.216.34');\n        });\n\n        Http::fake([\n            'https://free.freeipapi.com/api/json/93.184.216.34' => Http::response([\n                'countryCode' => 'UK',\n                'latitude' => 51.5074,\n                'longitude' => -0.1278,\n            ]),\n        ]);\n\n        $fetchGeolocation = app(FetchGeolocation::class);\n\n        $result = $fetchGeolocation->fetch('https://example.com/path/to/resource');\n\n        $this->assertNotNull($result);\n        $this->assertEquals('UK', $result['country']);\n    }\n\n    public function test_it_extracts_hostname_from_host_port_format(): void\n    {\n        Http::fake([\n            'https://free.freeipapi.com/api/json/192.168.1.1' => Http::response([\n                'countryCode' => 'DE',\n                'latitude' => 52.5200,\n                'longitude' => 13.4050,\n            ]),\n        ]);\n\n        $fetchGeolocation = app(FetchGeolocation::class);\n\n        $result = $fetchGeolocation->fetch('192.168.1.1:8080');\n\n        $this->assertNotNull($result);\n        $this->assertEquals('DE', $result['country']);\n    }\n\n    public function test_it_returns_null_on_api_failure(): void\n    {\n        $this->mock(ResolveRecord::class, function (MockInterface $mock) {\n            $mock->shouldReceive('resolve')->andReturn('93.184.216.34');\n        });\n\n        Http::fake([\n            'https://free.freeipapi.com/api/json/*' => Http::response([], 500),\n        ]);\n\n        $fetchGeolocation = app(FetchGeolocation::class);\n\n        $result = $fetchGeolocation->fetch('example.com');\n\n        $this->assertNull($result);\n    }\n\n    public function test_it_returns_null_on_exception(): void\n    {\n        $this->mock(ResolveRecord::class, function (MockInterface $mock) {\n            $mock->shouldReceive('resolve')->andReturn('93.184.216.34');\n        });\n\n        Http::fake([\n            'https://free.freeipapi.com/api/json/*' => function () {\n                throw new \\Exception('Network error');\n            },\n        ]);\n\n        $fetchGeolocation = app(FetchGeolocation::class);\n\n        $result = $fetchGeolocation->fetch('example.com');\n\n        $this->assertNull($result);\n    }\n}\n"
  },
  {
    "path": "packages/uptime/tests/Unit/GenerateOutpostCertificateTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests\\Unit;\n\nuse Illuminate\\Support\\Facades\\Storage;\nuse Vigilant\\Uptime\\Actions\\Outpost\\GenerateOutpostCertificate;\nuse Vigilant\\Uptime\\Actions\\Outpost\\GenerateRootCertificate;\nuse Vigilant\\Uptime\\Tests\\TestCase;\n\nclass GenerateOutpostCertificateTest extends TestCase\n{\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        // Clean up any existing certificates\n        Storage::disk('local')->delete([\n            'certificates/root-ca.key',\n            'certificates/root-ca.crt',\n        ]);\n    }\n\n    public function test_it_generates_root_ca_certificate(): void\n    {\n        $generator = new GenerateRootCertificate;\n\n        $this->assertFalse($generator->exists());\n\n        $generator->generate();\n\n        $this->assertTrue($generator->exists());\n\n        // Verify the certificate is valid\n        $cert = $generator->getRootCertificate();\n        $this->assertStringContainsString('BEGIN CERTIFICATE', $cert);\n\n        // Verify the private key is valid\n        $key = $generator->getRootPrivateKey();\n        $this->assertStringContainsString('BEGIN PRIVATE KEY', $key);\n    }\n\n    public function test_it_generates_outpost_certificate(): void\n    {\n        $rootGenerator = new GenerateRootCertificate;\n        $rootGenerator->generate();\n\n        $outpostGenerator = new GenerateOutpostCertificate($rootGenerator);\n\n        $certificate = $outpostGenerator->generate('test-outpost-192.168.1.1-8080', '192.168.1.1', 30);\n\n        $this->assertArrayHasKey('certificate', $certificate);\n        $this->assertArrayHasKey('private_key', $certificate);\n        $this->assertArrayHasKey('root_certificate', $certificate);\n\n        // Verify the certificate is valid\n        $this->assertStringContainsString('BEGIN CERTIFICATE', $certificate['certificate']);\n        $this->assertStringContainsString('BEGIN PRIVATE KEY', $certificate['private_key']);\n        $this->assertStringContainsString('BEGIN CERTIFICATE', $certificate['root_certificate']);\n\n        // Verify the certificate can be parsed\n        $certResource = openssl_x509_read($certificate['certificate']);\n        $this->assertNotFalse($certResource);\n\n        $certData = openssl_x509_parse($certResource);\n        $this->assertNotFalse($certData);\n        $this->assertEquals('test-outpost-192.168.1.1-8080', $certData['subject']['CN']);\n    }\n\n    protected function tearDown(): void\n    {\n        // Clean up certificates after test\n        Storage::disk('local')->delete([\n            'certificates/root-ca.key',\n            'certificates/root-ca.crt',\n        ]);\n\n        parent::tearDown();\n    }\n}\n"
  },
  {
    "path": "packages/uptime/tests/Unit/OutpostAuthMiddlewareTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests\\Unit;\n\nuse Illuminate\\Http\\Request;\nuse Vigilant\\Uptime\\Http\\Middleware\\OutpostAuthMiddleware;\nuse Vigilant\\Uptime\\Tests\\TestCase;\n\nclass OutpostAuthMiddlewareTest extends TestCase\n{\n    public function test_it_allows_request_with_valid_token(): void\n    {\n        config(['uptime.outpost_token' => 'valid-token']);\n\n        $middleware = new OutpostAuthMiddleware;\n        $request = Request::create('/test', 'GET');\n        $request->headers->set('X-Outpost-Token', 'valid-token');\n\n        $response = $middleware->handle($request, function ($req) {\n            return response()->json(['success' => true]);\n        });\n\n        $this->assertEquals(200, $response->getStatusCode());\n        $this->assertEquals(['success' => true], $response->getData(true));\n    }\n\n    public function test_it_denies_request_with_invalid_token(): void\n    {\n        config(['uptime.outpost_token' => 'valid-token']);\n\n        $middleware = new OutpostAuthMiddleware;\n        $request = Request::create('/test', 'GET');\n        $request->headers->set('X-Outpost-Token', 'invalid-token');\n\n        $response = $middleware->handle($request, function ($req) {\n            return response()->json(['success' => true]);\n        });\n\n        $this->assertEquals(401, $response->getStatusCode());\n        $this->assertEquals(['message' => 'Unauthorized'], $response->getData(true));\n    }\n\n    public function test_it_denies_request_without_token(): void\n    {\n        config(['uptime.outpost_token' => 'valid-token']);\n\n        $middleware = new OutpostAuthMiddleware;\n        $request = Request::create('/test', 'GET');\n\n        $response = $middleware->handle($request, function ($req) {\n            return response()->json(['success' => true]);\n        });\n\n        $this->assertEquals(401, $response->getStatusCode());\n        $this->assertEquals(['message' => 'Unauthorized'], $response->getData(true));\n    }\n}\n"
  },
  {
    "path": "packages/uptime/tests/Unit/RegisterOutpostTest.php",
    "content": "<?php\n\nnamespace Vigilant\\Uptime\\Tests\\Unit;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Vigilant\\Uptime\\Actions\\Outpost\\RegisterOutpost;\nuse Vigilant\\Uptime\\Enums\\OutpostStatus;\nuse Vigilant\\Uptime\\Models\\Outpost;\nuse Vigilant\\Uptime\\Tests\\TestCase;\n\nclass RegisterOutpostTest extends TestCase\n{\n    public function test_it_registers_new_outpost_with_geolocation(): void\n    {\n        Http::fake([\n            'https://free.freeipapi.com/api/json/*' => Http::response([\n                'countryCode' => 'US',\n                'latitude' => 40.7128,\n                'longitude' => -74.0060,\n            ]),\n        ]);\n\n        $registerOutpost = app(RegisterOutpost::class);\n\n        $outpost = $registerOutpost->register('1.2.3.4', '192.168.1.1', 8080);\n\n        $this->assertInstanceOf(Outpost::class, $outpost);\n        $this->assertEquals('1.2.3.4', $outpost->external_ip);\n        $this->assertEquals('192.168.1.1', $outpost->ip);\n        $this->assertEquals(8080, $outpost->port);\n        $this->assertEquals('US', $outpost->country);\n        $this->assertEquals(40.7128, $outpost->latitude);\n        $this->assertEquals(-74.0060, $outpost->longitude);\n        $this->assertEquals(OutpostStatus::Available, $outpost->status);\n    }\n\n    public function test_it_updates_existing_outpost_with_country(): void\n    {\n        $existingOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.1',\n            'port' => 8080,\n            'external_ip' => '1.2.3.4',\n            'status' => OutpostStatus::Unavailable,\n            'country' => 'US',\n            'latitude' => 40.7128,\n            'longitude' => -74.0060,\n            'last_available_at' => now()->subHour(),\n        ]);\n\n        Http::fake();\n\n        $registerOutpost = app(RegisterOutpost::class);\n\n        $outpost = $registerOutpost->register('1.2.3.5', '192.168.1.1', 8080);\n\n        $this->assertEquals($existingOutpost->id, $outpost->id);\n        $this->assertEquals('1.2.3.5', $outpost->external_ip);\n        $this->assertEquals(OutpostStatus::Available, $outpost->status);\n        $this->assertEquals('US', $outpost->country);\n\n        // Should not have made an HTTP request since country already exists\n        Http::assertNothingSent();\n    }\n\n    public function test_it_fetches_geolocation_for_existing_outpost_without_country(): void\n    {\n        $existingOutpost = Outpost::query()->create([\n            'ip' => '192.168.1.1',\n            'port' => 8080,\n            'external_ip' => '1.2.3.4',\n            'status' => OutpostStatus::Unavailable,\n            'country' => null,\n            'last_available_at' => now()->subHour(),\n        ]);\n\n        Http::fake([\n            'https://free.freeipapi.com/api/json/*' => Http::response([\n                'countryCode' => 'UK',\n                'latitude' => 51.5074,\n                'longitude' => -0.1278,\n            ]),\n        ]);\n\n        $registerOutpost = app(RegisterOutpost::class);\n\n        $outpost = $registerOutpost->register('1.2.3.5', '192.168.1.1', 8080);\n\n        $this->assertEquals($existingOutpost->id, $outpost->id);\n        $this->assertEquals('UK', $outpost->country);\n        $this->assertEquals(51.5074, $outpost->latitude);\n        $this->assertEquals(-0.1278, $outpost->longitude);\n\n        Http::assertSent(function ($request) {\n            return str_contains($request->url(), 'free.freeipapi.com');\n        });\n    }\n\n    public function test_it_handles_geolocation_fetch_failure_gracefully(): void\n    {\n        Http::fake([\n            'https://free.freeipapi.com/api/json/*' => Http::response([], 500),\n        ]);\n\n        $registerOutpost = app(RegisterOutpost::class);\n\n        $outpost = $registerOutpost->register('1.2.3.4', '192.168.1.1', 8080);\n\n        $this->assertInstanceOf(Outpost::class, $outpost);\n        $this->assertEquals('1.2.3.4', $outpost->external_ip);\n        $this->assertNull($outpost->country);\n        $this->assertNull($outpost->latitude);\n        $this->assertNull($outpost->longitude);\n        $this->assertEquals(OutpostStatus::Available, $outpost->status);\n    }\n\n    public function test_it_registers_outpost_with_manual_location_when_geoip_disabled(): void\n    {\n        Http::fake();\n\n        $registerOutpost = app(RegisterOutpost::class);\n\n        $outpost = $registerOutpost->register(\n            externalIp: '5.6.7.8',\n            ip: '10.0.0.1',\n            port: 9000,\n            geoipAutomatic: false,\n            country: 'ca',\n            latitude: 45.1234,\n            longitude: -75.9876,\n        );\n\n        $this->assertInstanceOf(Outpost::class, $outpost);\n        $this->assertEquals('5.6.7.8', $outpost->external_ip);\n        $this->assertEquals('10.0.0.1', $outpost->ip);\n        $this->assertEquals(9000, $outpost->port);\n        $this->assertEquals('CA', $outpost->country);\n        $this->assertEquals(45.1234, $outpost->latitude);\n        $this->assertEquals(-75.9876, $outpost->longitude);\n        $this->assertEquals(OutpostStatus::Available, $outpost->status);\n        $this->assertFalse((bool) $outpost->geoip_automatic);\n\n        Http::assertNothingSent();\n    }\n\n    public function test_it_updates_existing_outpost_with_manual_location_when_geoip_disabled(): void\n    {\n        $existingOutpost = Outpost::query()->create([\n            'ip' => '172.16.0.1',\n            'port' => 7070,\n            'external_ip' => '2.2.2.2',\n            'status' => OutpostStatus::Unavailable,\n            'country' => 'US',\n            'latitude' => 10.0,\n            'longitude' => 20.0,\n            'geoip_automatic' => true,\n            'last_available_at' => now()->subDay(),\n        ]);\n\n        Http::fake();\n\n        $registerOutpost = app(RegisterOutpost::class);\n\n        $outpost = $registerOutpost->register(\n            externalIp: '9.9.9.9',\n            ip: '172.16.0.1',\n            port: 7070,\n            geoipAutomatic: false,\n            country: 'br',\n            latitude: -23.5505,\n            longitude: -46.6333,\n        );\n\n        $this->assertEquals($existingOutpost->id, $outpost->id);\n        $this->assertEquals('9.9.9.9', $outpost->external_ip);\n        $this->assertEquals('BR', $outpost->country);\n        $this->assertEquals(-23.5505, $outpost->latitude);\n        $this->assertEquals(-46.6333, $outpost->longitude);\n        $this->assertEquals(OutpostStatus::Available, $outpost->status);\n        $this->assertFalse((bool) $outpost->geoip_automatic);\n\n        Http::assertNothingSent();\n    }\n}\n"
  },
  {
    "path": "packages/users/.gitignore",
    "content": "vendor\ncomposer.lock\n.phpunit.result.cache\n"
  },
  {
    "path": "packages/users/composer.json",
    "content": "{\n    \"name\": \"vigilant/users\",\n    \"description\": \"Vigilant Users\",\n    \"type\": \"package\",\n    \"license\": \"AGPL\",\n    \"authors\": [\n        {\n            \"name\": \"Vincent Boon\",\n            \"email\": \"info@vincentbean.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.3\",\n        \"laravel/framework\": \"^12.0\",\n        \"laravel/jetstream\": \"^5.0\",\n        \"laravel/sanctum\": \"^v4.0\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.6\",\n        \"larastan/larastan\": \"^3.0\",\n        \"orchestra/testbench\": \"^10.0\",\n        \"phpstan/phpstan-mockery\": \"^2.0\",\n        \"phpunit/phpunit\": \"^11.0\",\n        \"vigilant/core\": \"@dev\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Users\\\\\": \"src\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Vigilant\\\\Users\\\\Tests\\\\\": \"tests\",\n            \"Vigilant\\\\Users\\\\Database\\\\Factories\\\\\": \"database/factories\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"phpunit\",\n        \"analyse\": \"phpstan\",\n        \"style\": \"pint --test\",\n        \"quality\": [\n            \"@test\",\n            \"@analyse\"\n        ]\n    },\n    \"config\": {\n        \"sort-packages\": true,\n        \"allow-plugins\": {\n            \"php-http/discovery\": true\n        }\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Vigilant\\\\Users\\\\ServiceProvider\"\n            ]\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"../*\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/users/config/users.php",
    "content": "<?php\n\nreturn [\n    'queue' => 'default',\n];\n"
  },
  {
    "path": "packages/users/database/factories/TeamFactory.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Database\\Factories;\n\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Vigilant\\Users\\Models\\Team;\n\n/**\n * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\Vigilant\\Users\\Models\\Team>\n */\nclass TeamFactory extends Factory\n{\n    protected $model = Team::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array<string, mixed>\n     */\n    public function definition(): array\n    {\n        return [\n            'name' => $this->faker->unique()->company(),\n            'user_id' => 1,\n            'personal_team' => true,\n        ];\n    }\n}\n"
  },
  {
    "path": "packages/users/database/factories/UserFactory.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Database\\Factories;\n\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Support\\Str;\nuse Laravel\\Jetstream\\Features;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Models\\User;\n\n/**\n * @extends \\Illuminate\\Database\\Eloquent\\Factories\\Factory<\\Vigilant\\Users\\Models\\User>\n */\nclass UserFactory extends Factory\n{\n    protected $model = User::class;\n\n    /**\n     * Define the model's default state.\n     *\n     * @return array<string, mixed>\n     */\n    public function definition(): array\n    {\n        return [\n            'name' => $this->faker->name(),\n            'email' => $this->faker->unique()->safeEmail(),\n            'email_verified_at' => now(),\n            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password\n            'two_factor_secret' => null,\n            'two_factor_recovery_codes' => null,\n            'remember_token' => Str::random(10),\n            'profile_photo_path' => null,\n            'current_team_id' => null,\n        ];\n    }\n\n    /**\n     * Indicate that the model's email address should be unverified.\n     */\n    public function unverified(): static\n    {\n        return $this->state(function (array $attributes) {\n            return [\n                'email_verified_at' => null,\n            ];\n        });\n    }\n\n    /**\n     * Indicate that the user should have a personal team.\n     */\n    public function withPersonalTeam(?callable $callback = null): static\n    {\n        if (! Features::hasTeamFeatures()) {\n            return $this->state([]);\n        }\n\n        return $this->has(\n            Team::factory()\n                ->state(fn (array $attributes, User $user) => [\n                    'name' => $user->name.'\\'s Team',\n                    'user_id' => $user->id,\n                    'personal_team' => true,\n                ])\n                ->when(is_callable($callback), $callback),\n            'ownedTeams'\n        );\n    }\n}\n"
  },
  {
    "path": "packages/users/database/migrations/2014_10_12_000000_create_users_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('users', function (Blueprint $table) {\n            $table->id();\n            $table->string('name');\n            $table->string('email')->unique();\n            $table->timestamp('email_verified_at')->nullable();\n            $table->string('password');\n            $table->rememberToken();\n            $table->foreignId('current_team_id')->nullable();\n            $table->string('profile_photo_path', 2048)->nullable();\n            $table->timestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('users');\n    }\n};\n"
  },
  {
    "path": "packages/users/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('password_reset_tokens', function (Blueprint $table) {\n            $table->string('email')->primary();\n            $table->string('token');\n            $table->timestamp('created_at')->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('password_reset_tokens');\n    }\n};\n"
  },
  {
    "path": "packages/users/database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\nuse Laravel\\Fortify\\Fortify;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::table('users', function (Blueprint $table) {\n            $table->text('two_factor_secret')\n                ->after('password')\n                ->nullable();\n\n            $table->text('two_factor_recovery_codes')\n                ->after('two_factor_secret')\n                ->nullable();\n\n            if (Fortify::confirmsTwoFactorAuthentication()) {\n                $table->timestamp('two_factor_confirmed_at')\n                    ->after('two_factor_recovery_codes')\n                    ->nullable();\n            }\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::table('users', function (Blueprint $table) {\n            $table->dropColumn(array_merge([\n                'two_factor_secret',\n                'two_factor_recovery_codes',\n            ], Fortify::confirmsTwoFactorAuthentication() ? [\n                'two_factor_confirmed_at',\n            ] : []));\n        });\n    }\n};\n"
  },
  {
    "path": "packages/users/database/migrations/2020_05_21_100000_create_teams_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('teams', function (Blueprint $table) {\n            $table->id();\n            $table->foreignId('user_id')->index();\n            $table->string('name');\n            $table->boolean('personal_team');\n            $table->timestamps();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('teams');\n    }\n};\n"
  },
  {
    "path": "packages/users/database/migrations/2020_05_21_200000_create_team_user_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('team_user', function (Blueprint $table) {\n            $table->id();\n            $table->foreignId('team_id');\n            $table->foreignId('user_id');\n            $table->string('role')->nullable();\n            $table->timestamps();\n\n            $table->unique(['team_id', 'user_id']);\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('team_user');\n    }\n};\n"
  },
  {
    "path": "packages/users/database/migrations/2020_05_21_300000_create_team_invitations_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('team_invitations', function (Blueprint $table) {\n            $table->id();\n            $table->foreignId('team_id')->constrained()->cascadeOnDelete();\n            $table->string('email');\n            $table->string('role')->nullable();\n            $table->timestamps();\n\n            $table->unique(['team_id', 'email']);\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('team_invitations');\n    }\n};\n"
  },
  {
    "path": "packages/users/database/migrations/2024_05_18_100000_team_timezone_field.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration\n{\n    public function up(): void\n    {\n        Schema::table('teams', function (Blueprint $table) {\n            $table->string('timezone')->after('personal_team')->default('UTC');\n        });\n    }\n\n    public function down(): void\n    {\n        Schema::dropColumns('teams', ['timezone']);\n    }\n};\n"
  },
  {
    "path": "packages/users/phpstan.neon",
    "content": "includes:\n    - ./vendor/larastan/larastan/extension.neon\n    - ./vendor/phpstan/phpstan-mockery/extension.neon\n\nparameters:\n    paths:\n        - src\n        - tests\n    level: 8\n    ignoreErrors:\n        - identifier: missingType.iterableValue\n        - identifier: missingType.generics\n"
  },
  {
    "path": "packages/users/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\" bootstrap=\"vendor/autoload.php\" colors=\"true\">\n  <testsuites>\n    <testsuite name=\"Tests\">\n      <directory>./tests/*</directory>\n    </testsuite>\n  </testsuites>\n  <coverage/>\n  <source>\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "packages/users/routes/auth.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Vigilant\\Users\\Http\\Controllers\\SocialiteController;\n\nRoute::get('authenticate/{provider}', [SocialiteController::class, 'redirect'])->name('login.socialite');\nRoute::get('authenticate/callback/{provider}', [SocialiteController::class, 'callback'])->name('login.socialite.callback');\n"
  },
  {
    "path": "packages/users/routes/web.php",
    "content": "<?php\n\nuse Illuminate\\Foundation\\Auth\\EmailVerificationRequest;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Route;\n\nRoute::get('/email/verify', function () {\n    /** @var \\Vigilant\\Users\\Models\\User $user */\n    $user = auth()->user();\n\n    abort_if($user->hasVerifiedEmail(), 401, 'E-mail already verified');\n\n    /** @var view-string $view */\n    $view = 'auth.verify-email';\n\n    return view($view);\n})->middleware('auth')->name('verification.notice');\n\nRoute::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) {\n    $request->fulfill();\n\n    return redirect()->route('onboard');\n})->middleware(['auth', 'signed'])->name('verification.verify');\n\nRoute::post('/email/verification-notification', function (Request $request) {\n    $request->user()->sendEmailVerificationNotification();\n\n    return back()->with('message', 'Verification link sent!');\n})->middleware(['auth', 'throttle:6,1'])->name('verification.send');\n"
  },
  {
    "path": "packages/users/src/Actions/Fortify/CreateNewUser.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Actions\\Fortify;\n\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Laravel\\Fortify\\Contracts\\CreatesNewUsers;\nuse Laravel\\Jetstream\\Jetstream;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Models\\User;\nuse Vigilant\\Users\\Validators\\RegistrationEnabledValidator;\n\nclass CreateNewUser implements CreatesNewUsers\n{\n    use PasswordValidationRules;\n\n    /**\n     * Create a newly registered user.\n     *\n     * @param  array<string, string>  $input\n     */\n    public function create(array $input, array $optional = []): User\n    {\n        $rules = [\n            'name' => ['required', 'string', 'max:255'],\n            'email' => ['required', 'string', 'email', 'max:255', 'unique:users', new RegistrationEnabledValidator],\n            'password' => $this->passwordRules(),\n            'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',\n        ];\n\n        if ($optional !== []) {\n            foreach ($optional as $key) {\n                unset($rules[$key]);\n            }\n        }\n\n        Validator::make($input, $rules)->validate();\n\n        return DB::transaction(function () use ($input) {\n            return tap(User::create([\n                'name' => $input['name'],\n                'email' => $input['email'],\n                'password' => Hash::make($input['password']),\n            ]), function (User $user) {\n                $this->createTeam($user);\n            });\n        });\n    }\n\n    /**\n     * Create a personal team for the user.\n     */\n    protected function createTeam(User $user): void\n    {\n        $user->ownedTeams()->save(Team::forceCreate([\n            'user_id' => $user->id,\n            'name' => explode(' ', $user->name, 2)[0].\"'s Team\",\n            'personal_team' => true,\n        ]));\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Actions/Fortify/PasswordValidationRules.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Actions\\Fortify;\n\nuse Illuminate\\Validation\\Rules\\Password;\n\ntrait PasswordValidationRules\n{\n    /**\n     * Get the validation rules used to validate passwords.\n     *\n     * @return array<int, \\Illuminate\\Contracts\\Validation\\Rule|array|string>\n     */\n    protected function passwordRules(): array\n    {\n        return ['required', 'string', Password::default(), 'confirmed'];\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Actions/Fortify/ResetUserPassword.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Actions\\Fortify;\n\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Laravel\\Fortify\\Contracts\\ResetsUserPasswords;\nuse Vigilant\\Users\\Models\\User;\n\nclass ResetUserPassword implements ResetsUserPasswords\n{\n    use PasswordValidationRules;\n\n    /**\n     * Validate and reset the user's forgotten password.\n     *\n     * @param  array<string, string>  $input\n     */\n    public function reset(User $user, array $input): void\n    {\n        Validator::make($input, [\n            'password' => $this->passwordRules(),\n        ])->validate();\n\n        $user->forceFill([\n            'password' => Hash::make($input['password']),\n        ])->save();\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Actions/Fortify/UpdateUserPassword.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Actions\\Fortify;\n\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Laravel\\Fortify\\Contracts\\UpdatesUserPasswords;\nuse Vigilant\\Users\\Models\\User;\n\nclass UpdateUserPassword implements UpdatesUserPasswords\n{\n    use PasswordValidationRules;\n\n    /**\n     * Validate and update the user's password.\n     *\n     * @param  array<string, string>  $input\n     */\n    public function update(User $user, array $input): void\n    {\n        Validator::make($input, [\n            'current_password' => ['required', 'string', 'current_password:web'],\n            'password' => $this->passwordRules(),\n        ], [\n            'current_password.current_password' => __('The provided password does not match your current password.'),\n        ])->validateWithBag('updatePassword');\n\n        $user->forceFill([\n            'password' => Hash::make($input['password']),\n        ])->save();\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Actions/Fortify/UpdateUserProfileInformation.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Actions\\Fortify;\n\nuse Illuminate\\Contracts\\Auth\\MustVerifyEmail;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Illuminate\\Validation\\Rule;\nuse Laravel\\Fortify\\Contracts\\UpdatesUserProfileInformation;\nuse Vigilant\\Users\\Models\\User;\n\nclass UpdateUserProfileInformation implements UpdatesUserProfileInformation\n{\n    /**\n     * Validate and update the given user's profile information.\n     *\n     * @param  array<string, mixed>  $input\n     */\n    public function update(User $user, array $input): void\n    {\n        Validator::make($input, [\n            'name' => ['required', 'string', 'max:255'],\n            'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],\n            'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],\n        ])->validateWithBag('updateProfileInformation');\n\n        if (isset($input['photo'])) {\n            $user->updateProfilePhoto($input['photo']);\n        }\n\n        if ($input['email'] !== $user->email &&\n            $user instanceof MustVerifyEmail) { // @phpstan-ignore-line\n            $this->updateVerifiedUser($user, $input);\n        } else {\n            $user->forceFill([\n                'name' => $input['name'],\n                'email' => $input['email'],\n            ])->save();\n        }\n    }\n\n    /**\n     * Update the given verified user's profile information.\n     *\n     * @param  array<string, string>  $input\n     */\n    protected function updateVerifiedUser(User $user, array $input): void\n    {\n        $user->forceFill([\n            'name' => $input['name'],\n            'email' => $input['email'],\n            'email_verified_at' => null,\n        ])->save();\n\n        $user->sendEmailVerificationNotification();\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Actions/Jetstream/AddTeamMember.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Actions\\Jetstream;\n\nuse Closure;\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Laravel\\Jetstream\\Contracts\\AddsTeamMembers;\nuse Laravel\\Jetstream\\Events\\AddingTeamMember;\nuse Laravel\\Jetstream\\Events\\TeamMemberAdded;\nuse Laravel\\Jetstream\\Jetstream;\nuse Laravel\\Jetstream\\Rules\\Role;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Models\\User;\n\nclass AddTeamMember implements AddsTeamMembers\n{\n    /**\n     * Add a new team member to the given team.\n     */\n    public function add(User $user, Team $team, string $email, ?string $role = null): void\n    {\n        Gate::forUser($user)->authorize('addTeamMember', $team);\n\n        $this->validate($team, $email, $role);\n\n        $newTeamMember = Jetstream::findUserByEmailOrFail($email);\n\n        AddingTeamMember::dispatch($team, $newTeamMember);\n\n        $team->users()->attach(\n            $newTeamMember, ['role' => $role]\n        );\n\n        TeamMemberAdded::dispatch($team, $newTeamMember);\n    }\n\n    /**\n     * Validate the add member operation.\n     */\n    protected function validate(Team $team, string $email, ?string $role): void\n    {\n        Validator::make([\n            'email' => $email,\n            'role' => $role,\n        ], $this->rules(), [\n            'email.exists' => __('We were unable to find a registered user with this email address.'),\n        ])->after(\n            $this->ensureUserIsNotAlreadyOnTeam($team, $email)\n        )->validateWithBag('addTeamMember');\n    }\n\n    /**\n     * Get the validation rules for adding a team member.\n     *\n     * @return array<string, \\Illuminate\\Contracts\\Validation\\Rule|array|string>\n     */\n    protected function rules(): array\n    {\n        return array_filter([\n            'email' => ['required', 'email', 'exists:users'],\n            'role' => Jetstream::hasRoles()\n                            ? ['required', 'string', new Role]\n                            : null,\n        ]);\n    }\n\n    /**\n     * Ensure that the user is not already on the team.\n     */\n    protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure\n    {\n        return function ($validator) use ($team, $email) {\n            $validator->errors()->addIf(\n                $team->hasUserWithEmail($email),\n                'email',\n                __('This user already belongs to the team.')\n            );\n        };\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Actions/Jetstream/CreateTeam.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Actions\\Jetstream;\n\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Laravel\\Jetstream\\Contracts\\CreatesTeams;\nuse Laravel\\Jetstream\\Events\\AddingTeam;\nuse Laravel\\Jetstream\\Jetstream;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Models\\User;\n\nclass CreateTeam implements CreatesTeams\n{\n    /**\n     * Validate and create a new team for the given user.\n     *\n     * @param  array<string, string>  $input\n     */\n    public function create(User $user, array $input): Team\n    {\n        Gate::forUser($user)->authorize('create', Jetstream::newTeamModel());\n\n        Validator::make($input, [\n            'name' => ['required', 'string', 'max:255'],\n        ])->validateWithBag('createTeam');\n\n        AddingTeam::dispatch($user);\n\n        $user->switchTeam($team = $user->ownedTeams()->create([\n            'name' => $input['name'],\n            'personal_team' => false,\n        ]));\n\n        /** @var Team $team */\n        return $team;\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Actions/Jetstream/DeleteTeam.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Actions\\Jetstream;\n\nuse Laravel\\Jetstream\\Contracts\\DeletesTeams;\nuse Vigilant\\Users\\Models\\Team;\n\nclass DeleteTeam implements DeletesTeams\n{\n    /**\n     * Delete the given team.\n     */\n    public function delete(Team $team): void\n    {\n        $team->purge();\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Actions/Jetstream/DeleteUser.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Actions\\Jetstream;\n\nuse Illuminate\\Support\\Facades\\DB;\nuse Laravel\\Jetstream\\Contracts\\DeletesTeams;\nuse Laravel\\Jetstream\\Contracts\\DeletesUsers;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Models\\User;\n\nclass DeleteUser implements DeletesUsers\n{\n    /**\n     * The team deleter implementation.\n     *\n     * @var \\Laravel\\Jetstream\\Contracts\\DeletesTeams\n     */\n    protected $deletesTeams;\n\n    /**\n     * Create a new action instance.\n     */\n    public function __construct(DeletesTeams $deletesTeams)\n    {\n        $this->deletesTeams = $deletesTeams;\n    }\n\n    /**\n     * Delete the given user.\n     */\n    public function delete(User $user): void\n    {\n        DB::transaction(function () use ($user) {\n            $this->deleteTeams($user);\n            $user->deleteProfilePhoto();\n            $user->tokens->each->delete();\n            $user->delete();\n        });\n    }\n\n    /**\n     * Delete the teams and team associations attached to the user.\n     */\n    protected function deleteTeams(User $user): void\n    {\n        $user->teams()->detach();\n\n        $user->ownedTeams->each(function (Team $team): void { // @phpstan-ignore-line\n            $this->deletesTeams->delete($team);\n        });\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Actions/Jetstream/InviteTeamMember.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Actions\\Jetstream;\n\nuse Closure;\nuse Illuminate\\Database\\Query\\Builder;\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Mail;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Illuminate\\Validation\\Rule;\nuse Laravel\\Jetstream\\Contracts\\InvitesTeamMembers;\nuse Laravel\\Jetstream\\Events\\InvitingTeamMember;\nuse Laravel\\Jetstream\\Jetstream;\nuse Laravel\\Jetstream\\Mail\\TeamInvitation;\nuse Laravel\\Jetstream\\Rules\\Role;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Models\\User;\n\nclass InviteTeamMember implements InvitesTeamMembers\n{\n    /**\n     * Invite a new team member to the given team.\n     */\n    public function invite(User $user, Team $team, string $email, ?string $role = null): void\n    {\n        Gate::forUser($user)->authorize('addTeamMember', $team);\n\n        $this->validate($team, $email, $role);\n\n        InvitingTeamMember::dispatch($team, $email, $role);\n\n        /** @var \\Laravel\\Jetstream\\TeamInvitation $invitation */\n        $invitation = $team->teamInvitations()->create([\n            'email' => $email,\n            'role' => $role,\n        ]);\n\n        Mail::to($email)->send(new TeamInvitation($invitation));\n    }\n\n    /**\n     * Validate the invite member operation.\n     */\n    protected function validate(Team $team, string $email, ?string $role): void\n    {\n        Validator::make([\n            'email' => $email,\n            'role' => $role,\n        ], $this->rules($team), [\n            'email.unique' => __('This user has already been invited to the team.'),\n        ])->after(\n            $this->ensureUserIsNotAlreadyOnTeam($team, $email)\n        )->validateWithBag('addTeamMember');\n    }\n\n    /**\n     * Get the validation rules for inviting a team member.\n     *\n     * @return array<string, \\Illuminate\\Contracts\\Validation\\Rule|array|string>\n     */\n    protected function rules(Team $team): array\n    {\n        return array_filter([\n            'email' => [\n                'required', 'email',\n                Rule::unique('team_invitations')->where(function (Builder $query) use ($team) {\n                    $query->where('team_id', $team->id);\n                }),\n            ],\n            'role' => Jetstream::hasRoles()\n                            ? ['required', 'string', new Role]\n                            : null,\n        ]);\n    }\n\n    /**\n     * Ensure that the user is not already on the team.\n     */\n    protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure\n    {\n        return function ($validator) use ($team, $email) {\n            $validator->errors()->addIf(\n                $team->hasUserWithEmail($email),\n                'email',\n                __('This user already belongs to the team.')\n            );\n        };\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Actions/Jetstream/RemoveTeamMember.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Actions\\Jetstream;\n\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Validation\\ValidationException;\nuse Laravel\\Jetstream\\Contracts\\RemovesTeamMembers;\nuse Laravel\\Jetstream\\Events\\TeamMemberRemoved;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Models\\User;\n\nclass RemoveTeamMember implements RemovesTeamMembers\n{\n    /**\n     * Remove the team member from the given team.\n     */\n    public function remove(User $user, Team $team, User $teamMember): void\n    {\n        $this->authorize($user, $team, $teamMember);\n\n        $this->ensureUserDoesNotOwnTeam($teamMember, $team);\n\n        $team->removeUser($teamMember); // @phpstan-ignore-line - Jetstream docblock\n\n        TeamMemberRemoved::dispatch($team, $teamMember);\n    }\n\n    /**\n     * Authorize that the user can remove the team member.\n     */\n    protected function authorize(User $user, Team $team, User $teamMember): void\n    {\n        if (! Gate::forUser($user)->check('removeTeamMember', $team) &&\n            $user->id !== $teamMember->id) {\n            throw new AuthorizationException;\n        }\n    }\n\n    /**\n     * Ensure that the currently authenticated user does not own the team.\n     */\n    protected function ensureUserDoesNotOwnTeam(User $teamMember, Team $team): void\n    {\n        if ($teamMember->id === $team->owner?->id) {\n            throw ValidationException::withMessages([\n                'team' => [__('You may not leave a team that you created.')],\n            ])->errorBag('removeTeamMember');\n        }\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Actions/Jetstream/UpdateTeamName.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Actions\\Jetstream;\n\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Validator;\nuse Laravel\\Jetstream\\Contracts\\UpdatesTeamNames;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Models\\User;\n\nclass UpdateTeamName implements UpdatesTeamNames\n{\n    /**\n     * Validate and update the given team's name.\n     *\n     * @param  array<string, string>  $input\n     */\n    public function update(User $user, Team $team, array $input): void\n    {\n        Gate::forUser($user)->authorize('update', $team);\n\n        Validator::make($input, [\n            'name' => ['required', 'string', 'max:255'],\n            'timezone' => ['required', 'string', 'max:255', 'timezone:all'],\n        ])->validateWithBag('updateTeamName');\n\n        $team->forceFill([\n            'name' => $input['name'],\n            'timezone' => $input['timezone'],\n        ])->save();\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Http/Controllers/SocialiteController.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Http\\Controllers;\n\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\Routing\\Controller;\nuse Illuminate\\Support\\Facades\\Auth;\nuse Laravel\\Socialite\\Facades\\Socialite;\nuse Vigilant\\Users\\Actions\\Fortify\\CreateNewUser;\nuse Vigilant\\Users\\Models\\User;\n\nclass SocialiteController extends Controller\n{\n    public function redirect(string $provider): RedirectResponse\n    {\n        abort_if(! in_array($provider, ['google']), 404);\n\n        return Socialite::driver($provider)->redirect(); // @phpstan-ignore-line\n    }\n\n    public function callback(string $provider, CreateNewUser $creator): RedirectResponse\n    {\n        abort_if(! in_array($provider, ['google']), 404);\n\n        $socialiteUser = Socialite::driver($provider)->user(); // @phpstan-ignore-line\n\n        $user = User::query()\n            ->where('email', '=', $socialiteUser->getEmail())\n            ->first();\n\n        if ($user === null) {\n            $user = $creator->create([\n                'name' => $socialiteUser->getName(),\n                'email' => $socialiteUser->getEmail(),\n                'password' => str()->random(32),\n            ], ['terms', 'password']);\n        }\n\n        Auth::login($user);\n\n        return response()->redirectToRoute('sites');\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Http/Middleware/EnsureEmailIsVerified.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Contracts\\Auth\\MustVerifyEmail;\nuse Illuminate\\Support\\Facades\\Redirect;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\Facades\\URL;\n\nclass EnsureEmailIsVerified extends \\Illuminate\\Auth\\Middleware\\EnsureEmailIsVerified\n{\n    /**\n     * Handle an incoming request.\n     *\n     * @param  \\Illuminate\\Http\\Request  $request\n     * @param  string|null  $redirectToRoute\n     * @return \\Illuminate\\Http\\Response|\\Illuminate\\Http\\RedirectResponse|null\n     */\n    public function handle($request, Closure $next, $redirectToRoute = null)\n    {\n        if (ce() || Route::is('verification*') || $request->user() === null) {\n            return $next($request);\n        }\n\n        if ($request->user() instanceof MustVerifyEmail && ! $request->user()->hasVerifiedEmail()) {\n            if ($request->expectsJson()) {\n                abort(403, 'Your email address is not verified.');\n            } else {\n                return Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));\n            }\n        }\n\n        return $next($request);\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Http/Middleware/NoUserMiddleware.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Http\\Request;\nuse Vigilant\\Users\\Models\\User;\n\nclass NoUserMiddleware\n{\n    public function handle(Request $request, Closure $next): mixed\n    {\n        $isRegisterRoute = $request->routeIs('register')\n            || ($request->isMethod('POST') && str_ends_with($request->url(), 'register'));\n\n        if ($isRegisterRoute || auth()->check()) {\n            return $next($request);\n        }\n\n        $userCount = User::query()->count();\n\n        if ($userCount === 0) {\n            return redirect()->route('register');\n        }\n\n        return $next($request);\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Models/Membership.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Models;\n\nuse Laravel\\Jetstream\\Membership as JetstreamMembership;\n\nclass Membership extends JetstreamMembership\n{\n    /**\n     * Indicates if the IDs are auto-incrementing.\n     *\n     * @var bool\n     */\n    public $incrementing = true;\n}\n"
  },
  {
    "path": "packages/users/src/Models/Team.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Support\\Carbon;\nuse Laravel\\Jetstream\\Events\\TeamCreated;\nuse Laravel\\Jetstream\\Events\\TeamDeleted;\nuse Laravel\\Jetstream\\Events\\TeamUpdated;\nuse Laravel\\Jetstream\\Team as JetstreamTeam;\nuse Vigilant\\Users\\Database\\Factories\\TeamFactory;\n\n/**\n * @property int $id\n * @property int $user_id\n * @property string $name\n * @property bool $personal_team\n * @property string $timezone\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?User $owner\n */\nclass Team extends JetstreamTeam\n{\n    use HasFactory;\n\n    /**\n     * The attributes that should be cast.\n     *\n     * @var array<string, string>\n     */\n    protected $casts = [\n        'personal_team' => 'boolean',\n    ];\n\n    protected $fillable = [\n        'name',\n        'personal_team',\n    ];\n\n    /**\n     * The event map for the model.\n     *\n     * @var array<string, class-string>\n     */\n    protected $dispatchesEvents = [\n        'created' => TeamCreated::class,\n        'updated' => TeamUpdated::class,\n        'deleted' => TeamDeleted::class,\n    ];\n\n    protected static function newFactory(): TeamFactory\n    {\n        return TeamFactory::new();\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Models/TeamInvitation.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Laravel\\Jetstream\\TeamInvitation as JetstreamTeamInvitation;\n\nclass TeamInvitation extends JetstreamTeamInvitation\n{\n    /**\n     * The attributes that are mass assignable.\n     *\n     * @var array<int, string>\n     */\n    protected $fillable = [\n        'email',\n        'role',\n    ];\n\n    /**\n     * Get the team that the invitation belongs to.\n     */\n    public function team(): BelongsTo\n    {\n        return $this->belongsTo(Team::class);\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Models/User.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Models;\n\nuse Illuminate\\Contracts\\Auth\\MustVerifyEmail;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Foundation\\Auth\\User as Authenticatable;\nuse Illuminate\\Notifications\\Notifiable;\nuse Illuminate\\Support\\Carbon;\nuse Laravel\\Fortify\\TwoFactorAuthenticatable;\nuse Laravel\\Jetstream\\HasProfilePhoto;\nuse Laravel\\Jetstream\\HasTeams;\nuse Laravel\\Sanctum\\HasApiTokens;\nuse Vigilant\\Users\\Database\\Factories\\UserFactory;\nuse Vigilant\\Users\\Notifications\\VerifyEmail;\n\n/**\n * @property int $id\n * @property string $name\n * @property string $email\n * @property ?Carbon $email_verified_at\n * @property string $password\n * @property string $two_factor_secret\n * @property string $two_factor_recovery_codes\n * @property ?Carbon $two_factor_confirmed_at\n * @property string $remember_token\n * @property ?int $current_team_id\n * @property ?string $profile_photo_path\n * @property ?Carbon $created_at\n * @property ?Carbon $updated_at\n * @property ?Team $currentTeam\n */\nclass User extends Authenticatable implements MustVerifyEmail\n{\n    use HasApiTokens;\n    use HasFactory;\n    use HasProfilePhoto;\n    use HasTeams;\n    use Notifiable;\n    use TwoFactorAuthenticatable;\n\n    protected $fillable = [\n        'name', 'email', 'password',\n    ];\n\n    protected $hidden = [\n        'password',\n        'remember_token',\n        'two_factor_recovery_codes',\n        'two_factor_secret',\n    ];\n\n    /**\n     * The attributes that should be cast.\n     *\n     * @var array<string, string>\n     */\n    protected $casts = [\n        'email_verified_at' => 'datetime',\n    ];\n\n    protected $appends = [\n        'profile_photo_url',\n    ];\n\n    public function sendEmailVerificationNotification(): void\n    {\n        if (ce()) {\n            $this->markEmailAsVerified();\n\n            return;\n        }\n\n        $this->notify(new VerifyEmail);\n    }\n\n    protected static function newFactory(): UserFactory\n    {\n        return UserFactory::new();\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Notifications/VerifyEmail.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Notifications;\n\nuse Illuminate\\Auth\\Notifications\\VerifyEmail as BaseVerifyEmail;\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\n\nclass VerifyEmail extends BaseVerifyEmail implements ShouldQueue\n{\n    use Queueable;\n}\n"
  },
  {
    "path": "packages/users/src/Observers/TeamObserver.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Observers;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Vigilant\\Core\\Services\\TeamService;\n\nclass TeamObserver\n{\n    public function creating(Model $model): void\n    {\n        /** @var TeamService $teamService */\n        $teamService = app(TeamService::class);\n\n        // @phpstan-ignore-next-line\n        $model->team_id = $teamService->team()?->id;\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Policies/TeamPolicy.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Policies;\n\nuse Illuminate\\Auth\\Access\\HandlesAuthorization;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Models\\User;\n\nclass TeamPolicy\n{\n    use HandlesAuthorization;\n\n    /**\n     * Determine whether the user can view any models.\n     */\n    public function viewAny(User $user): bool\n    {\n        return true;\n    }\n\n    /**\n     * Determine whether the user can view the model.\n     */\n    public function view(User $user, Team $team): bool\n    {\n        return $user->belongsToTeam($team);\n    }\n\n    /**\n     * Determine whether the user can create models.\n     */\n    public function create(User $user): bool\n    {\n        return true;\n    }\n\n    /**\n     * Determine whether the user can update the model.\n     */\n    public function update(User $user, Team $team): bool\n    {\n        return $user->ownsTeam($team);\n    }\n\n    /**\n     * Determine whether the user can add team members.\n     */\n    public function addTeamMember(User $user, Team $team): bool\n    {\n        return $user->ownsTeam($team);\n    }\n\n    /**\n     * Determine whether the user can update team member permissions.\n     */\n    public function updateTeamMember(User $user, Team $team): bool\n    {\n        return $user->ownsTeam($team);\n    }\n\n    /**\n     * Determine whether the user can remove team members.\n     */\n    public function removeTeamMember(User $user, Team $team): bool\n    {\n        return $user->ownsTeam($team);\n    }\n\n    /**\n     * Determine whether the user can delete the model.\n     */\n    public function delete(User $user, Team $team): bool\n    {\n        return $user->ownsTeam($team);\n    }\n}\n"
  },
  {
    "path": "packages/users/src/ServiceProvider.php",
    "content": "<?php\n\nnamespace Vigilant\\Users;\n\nuse Illuminate\\Support\\Facades\\Gate;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider as BaseServiceProvider;\nuse Vigilant\\Core\\Services\\TeamService;\nuse Vigilant\\Users\\Models\\Team;\nuse Vigilant\\Users\\Policies\\TeamPolicy;\n\nclass ServiceProvider extends BaseServiceProvider\n{\n    public function register(): void\n    {\n        $this\n            ->registerConfig();\n    }\n\n    protected function registerConfig(): static\n    {\n        $this->mergeConfigFrom(__DIR__.'/../config/users.php', 'users');\n\n        return $this;\n    }\n\n    public function boot(): void\n    {\n        $this\n            ->bootServices()\n            ->bootRoutes()\n            ->bootConfig()\n            ->bootMigrations()\n            ->bootCommands()\n            ->bootPolicies();\n    }\n\n    protected function bootServices(): static\n    {\n        app()->singleton(TeamService::class);\n\n        return $this;\n    }\n\n    protected function bootRoutes(): static\n    {\n        if (! $this->app->routesAreCached()) {\n            Route::middleware(['web', 'auth'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/web.php'));\n\n            Route::middleware(['web'])\n                ->group(fn () => $this->loadRoutesFrom(__DIR__.'/../routes/auth.php'));\n        }\n\n        return $this;\n    }\n\n    protected function bootConfig(): static\n    {\n        $this->publishes([\n            __DIR__.'/../config/users.php' => config_path('users.php'),\n        ], 'config');\n\n        return $this;\n    }\n\n    protected function bootMigrations(): static\n    {\n        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');\n\n        return $this;\n    }\n\n    protected function bootCommands(): static\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n\n            ]);\n        }\n\n        return $this;\n    }\n\n    protected function bootPolicies(): static\n    {\n        if (ce()) {\n            Gate::policy(Team::class, TeamPolicy::class);\n        }\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "packages/users/src/Validators/RegistrationEnabledValidator.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Validators;\n\nuse Closure;\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\nuse Vigilant\\Users\\Models\\User;\n\nclass RegistrationEnabledValidator implements ValidationRule\n{\n    public function validate(string $attribute, mixed $value, Closure $fail): void\n    {\n        if (! ce()) {\n            return;\n        }\n\n        $userCount = User::query()->count();\n\n        if ($userCount > 0) {\n            $fail(__('Registration disabled, ask your administrator to create new accounts'));\n        }\n    }\n}\n"
  },
  {
    "path": "packages/users/testbench.yaml",
    "content": "providers:\n  - Vigilant\\Users\\ServiceProvider\n"
  },
  {
    "path": "packages/users/tests/TestCase.php",
    "content": "<?php\n\nnamespace Vigilant\\Users\\Tests;\n\nuse Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\nuse Vigilant\\Users\\ServiceProvider;\n\nabstract class TestCase extends BaseTestCase\n{\n    use LazilyRefreshDatabase;\n\n    protected function getPackageProviders($app): array\n    {\n        return [\n            ServiceProvider::class,\n        ];\n    }\n\n    protected function defineEnvironment($app): void\n    {\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => '',\n        ]);\n    }\n}\n"
  },
  {
    "path": "phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"vendor/phpunit/phpunit/phpunit.xsd\"\n         bootstrap=\"vendor/autoload.php\"\n         colors=\"true\"\n>\n    <testsuites>\n        <testsuite name=\"Unit\">\n            <directory>tests/Unit</directory>\n        </testsuite>\n        <testsuite name=\"Feature\">\n            <directory>tests/Feature</directory>\n        </testsuite>\n    </testsuites>\n    <source>\n        <include>\n            <directory>app</directory>\n        </include>\n    </source>\n    <php>\n        <env name=\"APP_ENV\" value=\"testing\"/>\n        <env name=\"BCRYPT_ROUNDS\" value=\"4\"/>\n        <env name=\"CACHE_DRIVER\" value=\"array\"/>\n        <!-- <env name=\"DB_CONNECTION\" value=\"sqlite\"/> -->\n        <!-- <env name=\"DB_DATABASE\" value=\":memory:\"/> -->\n        <env name=\"MAIL_MAILER\" value=\"array\"/>\n        <env name=\"PULSE_ENABLED\" value=\"false\"/>\n        <env name=\"QUEUE_CONNECTION\" value=\"sync\"/>\n        <env name=\"SESSION_DRIVER\" value=\"array\"/>\n        <env name=\"TELESCOPE_ENABLED\" value=\"false\"/>\n    </php>\n</phpunit>\n"
  },
  {
    "path": "postcss.config.js",
    "content": "export default {\n    plugins: {\n        '@tailwindcss/postcss': {},\n    },\n};\n"
  },
  {
    "path": "public/.htaccess",
    "content": "<IfModule mod_rewrite.c>\n    <IfModule mod_negotiation.c>\n        Options -MultiViews -Indexes\n    </IfModule>\n\n    RewriteEngine On\n\n    # Handle Authorization Header\n    RewriteCond %{HTTP:Authorization} .\n    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\n\n    # Redirect Trailing Slashes If Not A Folder...\n    RewriteCond %{REQUEST_FILENAME} !-d\n    RewriteCond %{REQUEST_URI} (.+)/$\n    RewriteRule ^ %1 [L,R=301]\n\n    # Send Requests To Front Controller...\n    RewriteCond %{REQUEST_FILENAME} !-d\n    RewriteCond %{REQUEST_FILENAME} !-f\n    RewriteRule ^ index.php [L]\n</IfModule>\n"
  },
  {
    "path": "public/index.php",
    "content": "<?php\n\nuse Illuminate\\Contracts\\Http\\Kernel;\nuse Illuminate\\Http\\Request;\n\ndefine('LARAVEL_START', microtime(true));\n\n/*\n|--------------------------------------------------------------------------\n| Check If The Application Is Under Maintenance\n|--------------------------------------------------------------------------\n|\n| If the application is in maintenance / demo mode via the \"down\" command\n| we will load this file so that any pre-rendered content can be shown\n| instead of starting the framework, which could cause an exception.\n|\n*/\n\nif (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {\n    require $maintenance;\n}\n\n/*\n|--------------------------------------------------------------------------\n| Register The Auto Loader\n|--------------------------------------------------------------------------\n|\n| Composer provides a convenient, automatically generated class loader for\n| this application. We just need to utilize it! We'll simply require it\n| into the script here so we don't need to manually load our classes.\n|\n*/\n\nrequire __DIR__.'/../vendor/autoload.php';\n\n/*\n|--------------------------------------------------------------------------\n| Run The Application\n|--------------------------------------------------------------------------\n|\n| Once we have the application, we can handle the incoming request using\n| the application's HTTP kernel. Then, we will send the response back\n| to this client's browser, allowing them to enjoy our application.\n|\n*/\n\n$app = require_once __DIR__.'/../bootstrap/app.php';\n\n$kernel = $app->make(Kernel::class);\n\n$response = $kernel->handle(\n    $request = Request::capture()\n)->send();\n\n$kernel->terminate($request, $response);\n"
  },
  {
    "path": "public/robots.txt",
    "content": "User-agent: *\nDisallow:\n"
  },
  {
    "path": "public/site.webmanifest",
    "content": "{\"name\":\"\",\"short_name\":\"\",\"icons\":[{\"src\":\"/android-chrome-192x192.png\",\"sizes\":\"192x192\",\"type\":\"image/png\"},{\"src\":\"/android-chrome-512x512.png\",\"sizes\":\"512x512\",\"type\":\"image/png\"}],\"theme_color\":\"#ffffff\",\"background_color\":\"#ffffff\",\"display\":\"standalone\"}"
  },
  {
    "path": "resources/css/app.css",
    "content": "@import './tailwind.sources.css';\n@import 'tailwindcss';\n\n@plugin '@tailwindcss/forms';\n@plugin '@tailwindcss/typography';\n\n@custom-variant dark (&:is(.dark, .dark *));\n\n@theme {\n  /* Base Colors (Grayscale) */\n  --color-base-black: #0A0A0F;\n  --color-base-950: #131318;\n  --color-base-900: #1A1A24;\n  --color-base-850: #232333;\n  --color-base-800: #2D2D42;\n  --color-base-700: #444459;\n  --color-base-600: #5C5C75;\n  --color-base-500: #7A7A94;\n  --color-base-400: #A8A8C0;\n  --color-base-300: #C8C8DC;\n  --color-base-200: #D8D8E8;\n  --color-base-150: #E8E8F4;\n  --color-base-100: #F4F4FA;\n  --color-base-50: #FAFAFF;\n  --color-base-paper: #FAFAFF;\n\n  /* Accent Colors - Red (Primary CTA) */\n  --color-red: #EF4444;\n  --color-red-light: #F87171;\n  --color-red-dark: #DC2626;\n\n  /* Accent Colors - Orange (Secondary CTA) */\n  --color-orange: #F97316;\n  --color-orange-light: #FB923C;\n  --color-orange-dark: #EA580C;\n\n  /* Accent Colors - Yellow (Warning) */\n  --color-yellow: #F59E0B;\n  --color-yellow-light: #FBBF24;\n  --color-yellow-dark: #D97706;\n\n  /* Accent Colors - Green (Success) */\n  --color-green: #10B981;\n  --color-green-light: #34D399;\n  --color-green-dark: #059669;\n\n  /* Accent Colors - Cyan */\n  --color-cyan: #06B6D4;\n  --color-cyan-light: #22D3EE;\n  --color-cyan-dark: #0891B2;\n\n  /* Accent Colors - Blue */\n  --color-blue: #3B82F6;\n  --color-blue-light: #60A5FA;\n  --color-blue-dark: #2563EB;\n\n  /* Accent Colors - Indigo */\n  --color-indigo: #6366F1;\n  --color-indigo-light: #818CF8;\n  --color-indigo-dark: #4F46E5;\n\n  /* Accent Colors - Purple */\n  --color-purple: #8B5CF6;\n  --color-purple-light: #A78BFA;\n  --color-purple-dark: #7C3AED;\n\n  /* Accent Colors - Magenta */\n  --color-magenta: #EC4899;\n  --color-magenta-light: #F472B6;\n  --color-magenta-dark: #DB2777;\n\n  --font-sans:\n    Figtree, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',\n    'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';\n}\n\n/*\n  The default border color has changed to `currentcolor` in Tailwind CSS v4,\n  so we've added these compatibility styles to make sure everything still\n  looks the same as it did with Tailwind CSS v3.\n\n  If we ever want to remove these styles, we need to add an explicit border\n  color utility to any element that depends on these defaults.\n*/\n@layer base {\n  *,\n  ::after,\n  ::before,\n  ::backdrop,\n  ::file-selector-button {\n    border-color: var(--color-gray-200, currentcolor);\n  }\n\n  /* Ensure all buttons have pointer cursor on hover unless disabled */\n  button:not(:disabled),\n  [type=\"button\"]:not(:disabled),\n  [type=\"submit\"]:not(:disabled),\n  [type=\"reset\"]:not(:disabled) {\n    cursor: pointer;\n  }\n\n  button:disabled,\n  [type=\"button\"]:disabled,\n  [type=\"submit\"]:disabled,\n  [type=\"reset\"]:disabled {\n    cursor: not-allowed;\n  }\n  \n  /* Vertical gradient animation */\n  @keyframes gradient-y {\n    0%, 100% {\n      background-position: 0% 0%;\n    }\n    50% {\n      background-position: 0% 100%;\n    }\n  }\n  \n  .animate-gradient-y {\n    background-size: 100% 200%;\n    animation: gradient-y 6s ease infinite;\n  }\n\n  /* Horizontal gradient shift animation for CTA buttons */\n  @keyframes gradient-shift {\n    0%, 100% {\n      background-position: 0% 50%;\n    }\n    50% {\n      background-position: 100% 50%;\n    }\n  }\n\n  .animate-gradient-shift {\n    animation: gradient-shift 3s ease infinite;\n  }\n\n  /* Background size for animated gradients */\n  .bg-300\\% {\n    background-size: 300% 300%;\n  }\n\n  /* Pulse glow animation for decorative elements */\n  @keyframes pulse-glow {\n    0%, 100% {\n      opacity: 0.3;\n      transform: scale(1);\n    }\n    50% {\n      opacity: 0.5;\n      transform: scale(1.05);\n    }\n  }\n\n  .animate-pulse-glow {\n    animation: pulse-glow 4s ease-in-out infinite;\n  }\n\n  /* Float animation for decorative elements */\n  @keyframes float {\n    0%, 100% {\n      transform: translateY(0px);\n    }\n    50% {\n      transform: translateY(-10px);\n    }\n  }\n\n  .animate-float {\n    animation: float 6s ease-in-out infinite;\n  }\n\n  /* Bell swing animation for notification bell */\n  @keyframes bell-swing {\n    0%, 100% {\n      transform: rotate(0deg);\n      transform-origin: top center;\n    }\n    10%, 30%, 50%, 70%, 90% {\n      transform: rotate(8deg);\n      transform-origin: top center;\n    }\n    20%, 40%, 60%, 80% {\n      transform: rotate(-8deg);\n      transform-origin: top center;\n    }\n  }\n\n  .animate-bell-swing {\n    animation: bell-swing 0.8s ease-in-out;\n  }\n\n  /* Bell swing on hover */\n  .notification-bell:hover svg {\n    animation: bell-swing 0.8s ease-in-out;\n  }\n\n  /* Fade in up animation for initial page load */\n  @keyframes fadeInUp {\n    from {\n      opacity: 0;\n      transform: translateY(1rem);\n    }\n    to {\n      opacity: 1;\n      transform: translateY(0);\n    }\n  }\n}\n\n[x-cloak] {\n    display: none;\n}\n\n.tooltip {\n    @apply invisible absolute;\n}\n\n.has-tooltip {\n    @apply relative;\n}\n\n.has-tooltip:hover .tooltip {\n    @apply visible z-50;\n}\n\n.tooltip-left {\n    @apply right-full mr-2 top-1/2 transform -translate-y-1/2 whitespace-nowrap;\n}\n\nbutton:disabled {\n    @apply cursor-not-allowed;\n}\n\n.font-header {\n    font-family: \"Audiowide\", sans-serif;\n    font-weight: 400;\n    font-style: normal;\n}\n\n.font-text {\n    font-family: \"Noto Sans\", sans-serif;\n    font-optical-sizing: auto;\n    font-weight: 400;\n    font-style: normal;\n    font-variation-settings: \"wdth\" 100;\n}\n\nselect:not([multiple]) option {\n    @apply bg-gray-950;\n}\n\n/* Safelist for dynamic color classes used in modal headers and other components */\n@layer utilities {\n  .bg-red\\/10 {\n    background-color: rgb(239 68 68 / 0.1);\n  }\n\n  .bg-blue\\/10 {\n    background-color: rgb(59 130 246 / 0.1);\n  }\n\n  .bg-green\\/10 {\n    background-color: rgb(16 185 129 / 0.1);\n  }\n\n  .bg-yellow\\/10 {\n    background-color: rgb(245 158 11 / 0.1);\n  }\n\n  .bg-purple\\/10 {\n    background-color: rgb(139 92 246 / 0.1);\n  }\n\n  .border-red\\/30 {\n    border-color: rgb(239 68 68 / 0.3);\n  }\n\n  .border-blue\\/30 {\n    border-color: rgb(59 130 246 / 0.3);\n  }\n\n  .border-green\\/30 {\n    border-color: rgb(16 185 129 / 0.3);\n  }\n\n  .border-yellow\\/30 {\n    border-color: rgb(245 158 11 / 0.3);\n  }\n\n  .border-purple\\/30 {\n    border-color: rgb(139 92 246 / 0.3);\n  }\n\n  .text-red {\n    color: rgb(239 68 68);\n  }\n\n  .text-blue {\n    color: rgb(59 130 246);\n  }\n\n  .text-green {\n    color: rgb(16 185 129);\n  }\n\n  .text-yellow {\n    color: rgb(245 158 11);\n  }\n\n  .text-purple {\n    color: rgb(139 92 246);\n  }\n}\n"
  },
  {
    "path": "resources/css/tailwind.sources.css",
    "content": "/*\n * This file is auto-generated by scripts/generate-tailwind-sources.mjs.\n * Do not edit this file manually.\n */\n@source '../../packages/certificates/resources/views/**/*.blade.php';\n@source '../../packages/crawler/resources/views/**/*.blade.php';\n@source '../../packages/cve/resources/views/**/*.blade.php';\n@source '../../packages/dns/resources/views/**/*.blade.php';\n@source '../../packages/frontend/resources/views/**/*.blade.php';\n@source '../../packages/healthchecks/resources/views/**/*.blade.php';\n@source '../../packages/lighthouse/resources/views/**/*.blade.php';\n@source '../../packages/notifications/resources/views/**/*.blade.php';\n@source '../../packages/onboarding/resources/views/**/*.blade.php';\n@source '../../packages/settings/resources/views/**/*.blade.php';\n@source '../../packages/sites/resources/views/**/*.blade.php';\n@source '../../packages/uptime/resources/views/**/*.blade.php';\n@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';\n@source '../../vendor/laravel/jetstream/**/*.blade.php';\n@source '../../vendor/ramonrietdijk/livewire-tables/resources/**/*.blade.php';\n@source '../views/**/*.blade.php';\n"
  },
  {
    "path": "resources/js/app.js",
    "content": "import './bootstrap';\n\nimport Chart from 'chart.js/auto';\n\nChart.defaults.color = 'rgb(209, 213, 219)';\nwindow.Chart = Chart;\n\n// Translate Livewire navigate events to Alpine-compatible state\ndocument.addEventListener('livewire:navigate', () => {\n    window.dispatchEvent(new CustomEvent('navigation-start'));\n});\n\ndocument.addEventListener('livewire:navigated', () => {\n    window.dispatchEvent(new CustomEvent('navigation-end'));\n});\n\n"
  },
  {
    "path": "resources/js/bootstrap.js",
    "content": "/**\n * We'll load the axios HTTP library which allows us to easily issue requests\n * to our Laravel back-end. This library automatically handles sending the\n * CSRF token as a header based on the value of the \"XSRF\" token cookie.\n */\n\nimport axios from 'axios';\nwindow.axios = axios;\n\nwindow.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';\n\n/**\n * Echo exposes an expressive API for subscribing to channels and listening\n * for events that are broadcast by Laravel. Echo and event broadcasting\n * allows your team to easily build robust real-time web applications.\n */\n\n// import Echo from 'laravel-echo';\n\n// import Pusher from 'pusher-js';\n// window.Pusher = Pusher;\n\n// window.Echo = new Echo({\n//     broadcaster: 'pusher',\n//     key: import.meta.env.VITE_PUSHER_APP_KEY,\n//     cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',\n//     wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,\n//     wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,\n//     wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,\n//     forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',\n//     enabledTransports: ['ws', 'wss'],\n// });\n"
  },
  {
    "path": "resources/lang/en/validation.php",
    "content": "<?php\n\nreturn [\n    'attributes' => [\n        'settings.webhook_url' => 'webhook URL',\n        'settings.to' => 'email address',\n        'settings.server' => 'server',\n        'settings.topic' => 'topic',\n    ],\n];\n"
  },
  {
    "path": "resources/markdown/policy.md",
    "content": "# Privacy Policy\n\nEdit this file to define the privacy policy for your application.\n"
  },
  {
    "path": "resources/markdown/terms.md",
    "content": "# Terms of Service\n\nEdit this file to define the terms of service for your application.\n"
  },
  {
    "path": "resources/navigation.php",
    "content": "<?php\n\nuse Vigilant\\Core\\Facades\\Navigation;\n\n// Navigation::add(route('dashboard'), 'Dashboard')\n//    ->icon('tni-area-chart-alt-o');\n"
  },
  {
    "path": "resources/views/api/api-token-manager.blade.php",
    "content": "<div>\n    <!-- Generate API Token -->\n    <x-form-section submit=\"createApiToken\">\n        <x-slot name=\"title\">\n            {{ __('Create API Token') }}\n        </x-slot>\n\n        <x-slot name=\"description\">\n            {{ __('API tokens allow third-party services to authenticate with our application on your behalf.') }}\n        </x-slot>\n\n        <x-slot name=\"form\">\n            <!-- Token Name -->\n            <div class=\"col-span-6 sm:col-span-4\">\n                <x-label for=\"name\" value=\"{{ __('Token Name') }}\" />\n                <x-input id=\"name\" type=\"text\" class=\"mt-1 block w-full\" wire:model=\"createApiTokenForm.name\" autofocus />\n                <x-input-error for=\"name\" class=\"mt-2\" />\n            </div>\n\n            <!-- Token Permissions -->\n            @if (Laravel\\Jetstream\\Jetstream::hasPermissions())\n                <div class=\"col-span-6\">\n                    <x-label for=\"permissions\" value=\"{{ __('Permissions') }}\" />\n\n                    <div class=\"mt-2 grid grid-cols-1 md:grid-cols-2 gap-4\">\n                        @foreach (Laravel\\Jetstream\\Jetstream::$permissions as $permission)\n                            <label class=\"flex items-center\">\n                                <x-checkbox wire:model=\"createApiTokenForm.permissions\" :value=\"$permission\"/>\n                                <span class=\"ms-2 text-sm text-gray-600\">{{ $permission }}</span>\n                            </label>\n                        @endforeach\n                    </div>\n                </div>\n            @endif\n        </x-slot>\n\n        <x-slot name=\"actions\">\n            <x-action-message class=\"me-3\" on=\"created\">\n                {{ __('Created.') }}\n            </x-action-message>\n\n            <x-button>\n                {{ __('Create') }}\n            </x-button>\n        </x-slot>\n    </x-form-section>\n\n    @if ($this->user->tokens->isNotEmpty())\n        <x-section-border />\n\n        <!-- Manage API Tokens -->\n        <div class=\"mt-10 sm:mt-0\">\n            <x-action-section>\n                <x-slot name=\"title\">\n                    {{ __('Manage API Tokens') }}\n                </x-slot>\n\n                <x-slot name=\"description\">\n                    {{ __('You may delete any of your existing tokens if they are no longer needed.') }}\n                </x-slot>\n\n                <!-- API Token List -->\n                <x-slot name=\"content\">\n                    <div class=\"space-y-6\">\n                        @foreach ($this->user->tokens->sortBy('name') as $token)\n                            <div class=\"flex items-center justify-between\">\n                                <div class=\"break-all\">\n                                    {{ $token->name }}\n                                </div>\n\n                                <div class=\"flex items-center ms-2\">\n                                    @if ($token->last_used_at)\n                                        <div class=\"text-sm text-gray-400\">\n                                            {{ __('Last used') }} {{ $token->last_used_at->diffForHumans() }}\n                                        </div>\n                                    @endif\n\n                                    @if (Laravel\\Jetstream\\Jetstream::hasPermissions())\n                                        <button class=\"cursor-pointer ms-6 text-sm text-gray-400 underline\" wire:click=\"manageApiTokenPermissions({{ $token->id }})\">\n                                            {{ __('Permissions') }}\n                                        </button>\n                                    @endif\n\n                                    <button class=\"cursor-pointer ms-6 text-sm text-red-500\" wire:click=\"confirmApiTokenDeletion({{ $token->id }})\">\n                                        {{ __('Delete') }}\n                                    </button>\n                                </div>\n                            </div>\n                        @endforeach\n                    </div>\n                </x-slot>\n            </x-action-section>\n        </div>\n    @endif\n\n    <!-- Token Value Modal -->\n    <x-dialog-modal wire:model.live=\"displayingToken\">\n        <x-slot name=\"title\">\n            {{ __('API Token') }}\n        </x-slot>\n\n        <x-slot name=\"content\">\n            <div>\n                {{ __('Please copy your new API token. For your security, it won\\'t be shown again.') }}\n            </div>\n\n            <x-input x-ref=\"plaintextToken\" type=\"text\" readonly :value=\"$plainTextToken\"\n                class=\"mt-4 bg-gray-100 px-4 py-2 rounded-sm font-mono text-sm text-gray-500 w-full break-all\"\n                autofocus autocomplete=\"off\" autocorrect=\"off\" autocapitalize=\"off\" spellcheck=\"false\"\n                @showing-token-modal.window=\"setTimeout(() => $refs.plaintextToken.select(), 250)\"\n            />\n        </x-slot>\n\n        <x-slot name=\"footer\">\n            <x-secondary-button wire:click=\"$set('displayingToken', false)\" wire:loading.attr=\"disabled\">\n                {{ __('Close') }}\n            </x-secondary-button>\n        </x-slot>\n    </x-dialog-modal>\n\n    <!-- API Token Permissions Modal -->\n    <x-dialog-modal wire:model.live=\"managingApiTokenPermissions\">\n        <x-slot name=\"title\">\n            {{ __('API Token Permissions') }}\n        </x-slot>\n\n        <x-slot name=\"content\">\n            <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                @foreach (Laravel\\Jetstream\\Jetstream::$permissions as $permission)\n                    <label class=\"flex items-center\">\n                        <x-checkbox wire:model=\"updateApiTokenForm.permissions\" :value=\"$permission\"/>\n                        <span class=\"ms-2 text-sm text-gray-600\">{{ $permission }}</span>\n                    </label>\n                @endforeach\n            </div>\n        </x-slot>\n\n        <x-slot name=\"footer\">\n            <x-secondary-button wire:click=\"$set('managingApiTokenPermissions', false)\" wire:loading.attr=\"disabled\">\n                {{ __('Cancel') }}\n            </x-secondary-button>\n\n            <x-button class=\"ms-3\" wire:click=\"updateApiToken\" wire:loading.attr=\"disabled\">\n                {{ __('Save') }}\n            </x-button>\n        </x-slot>\n    </x-dialog-modal>\n\n    <!-- Delete Token Confirmation Modal -->\n    <x-confirmation-modal wire:model.live=\"confirmingApiTokenDeletion\">\n        <x-slot name=\"title\">\n            {{ __('Delete API Token') }}\n        </x-slot>\n\n        <x-slot name=\"content\">\n            {{ __('Are you sure you would like to delete this API token?') }}\n        </x-slot>\n\n        <x-slot name=\"footer\">\n            <x-secondary-button wire:click=\"$toggle('confirmingApiTokenDeletion')\" wire:loading.attr=\"disabled\">\n                {{ __('Cancel') }}\n            </x-secondary-button>\n\n            <x-danger-button class=\"ms-3\" wire:click=\"deleteApiToken\" wire:loading.attr=\"disabled\">\n                {{ __('Delete') }}\n            </x-danger-button>\n        </x-slot>\n    </x-confirmation-modal>\n</div>\n"
  },
  {
    "path": "resources/views/api/index.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <h2 class=\"font-semibold text-xl text-gray-800 leading-tight\">\n            {{ __('API Tokens') }}\n        </h2>\n    </x-slot>\n\n    <div>\n        <div class=\"max-w-7xl mx-auto py-10 sm:px-6 lg:px-8\">\n            @livewire('api.api-token-manager')\n        </div>\n    </div>\n</x-app-layout>\n"
  },
  {
    "path": "resources/views/auth/confirm-password.blade.php",
    "content": "<x-guest-layout>\n    <x-authentication-card>\n        <x-slot name=\"logo\">\n            <x-authentication-card-logo />\n        </x-slot>\n\n        <div class=\"mb-6 text-sm text-base-300 leading-relaxed\">\n            {{ __('This is a secure area of the application. Please confirm your password before continuing.') }}\n        </div>\n\n        <x-validation-errors class=\"mb-6\" />\n\n        <form method=\"POST\" action=\"{{ route('password.confirm') }}\">\n            @csrf\n\n            <div>\n                <x-label for=\"password\" value=\"{{ __('Password') }}\" />\n                <x-input id=\"password\" class=\"block mt-1 w-full\" type=\"password\" name=\"password\" required autocomplete=\"current-password\" autofocus />\n            </div>\n\n            <div class=\"mt-6\">\n                <button type=\"submit\"\n                    class=\"w-full inline-flex items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-red via-orange to-red bg-300% animate-gradient-shift px-6 py-2.5 text-sm font-semibold text-base-100 shadow-lg shadow-red/20 hover:shadow-xl hover:shadow-red/30 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-light disabled:opacity-50 transition-all duration-300\">\n                    {{ __('Confirm') }}\n                </button>\n            </div>\n        </form>\n    </x-authentication-card>\n</x-guest-layout>\n"
  },
  {
    "path": "resources/views/auth/forgot-password.blade.php",
    "content": "<x-guest-layout>\n    <x-authentication-card>\n        <x-slot name=\"logo\">\n            <x-authentication-card-logo />\n        </x-slot>\n\n        <div class=\"mb-6 text-sm text-base-300 leading-relaxed\">\n            {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}\n        </div>\n\n        @if (session('status'))\n            <div class=\"mb-6 font-medium text-sm text-green-light bg-green/10 border border-green/30 rounded-lg px-4 py-3\">\n                {{ session('status') }}\n            </div>\n        @endif\n\n        <x-validation-errors class=\"mb-6\" />\n\n        <form method=\"POST\" action=\"{{ route('password.email') }}\">\n            @csrf\n\n            <div class=\"block\">\n                <x-label for=\"email\" value=\"{{ __('Email') }}\" />\n                <x-input id=\"email\" class=\"block mt-1 w-full\" type=\"email\" name=\"email\" :value=\"old('email')\" required autofocus autocomplete=\"username\" />\n            </div>\n\n            <div class=\"mt-6\">\n                <button type=\"submit\"\n                    class=\"w-full inline-flex items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-red via-orange to-red bg-300% animate-gradient-shift px-6 py-2.5 text-sm font-semibold text-base-100 shadow-lg shadow-red/20 hover:shadow-xl hover:shadow-red/30 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-light disabled:opacity-50 transition-all duration-300\">\n                    {{ __('Email Password Reset Link') }}\n                </button>\n            </div>\n        </form>\n    </x-authentication-card>\n</x-guest-layout>\n"
  },
  {
    "path": "resources/views/auth/login.blade.php",
    "content": "<x-guest-layout>\n    <x-authentication-card>\n        <x-slot name=\"logo\">\n            <x-authentication-card-logo />\n        </x-slot>\n\n        <x-validation-errors class=\"mb-6\" />\n\n        @if (session('status'))\n            <div\n                class=\"mb-6 font-medium text-sm text-green-light bg-green/10 border border-green/30 rounded-lg px-4 py-3\">\n                {{ session('status') }}\n            </div>\n        @endif\n\n        <form method=\"POST\" action=\"{{ route('login') }}\">\n            @csrf\n\n            <div>\n                <x-label for=\"email\" value=\"{{ __('Email') }}\" />\n                <x-input id=\"email\" class=\"block mt-1 w-full\" type=\"email\" name=\"email\" :value=\"old('email')\" required\n                    autofocus autocomplete=\"username\" />\n            </div>\n\n            <div class=\"mt-4\">\n                <x-label for=\"password\" value=\"{{ __('Password') }}\" />\n                <x-input id=\"password\" class=\"block mt-1 w-full\" type=\"password\" name=\"password\" required\n                    autocomplete=\"current-password\" />\n            </div>\n\n            <div class=\"mt-6 flex justify-between items-center\">\n                <label for=\"remember_me\" class=\"flex items-center\">\n                    <x-checkbox id=\"remember_me\" name=\"remember\" />\n                    <span class=\"ms-2 text-sm text-base-200\">{{ __('Remember me') }}</span>\n                </label>\n            </div>\n\n            <div class=\"mt-6\">\n                <button type=\"submit\"\n                    class=\"w-full inline-flex items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-red via-orange to-red bg-300% animate-gradient-shift px-6 py-2.5 text-sm font-semibold text-base-100 shadow-lg shadow-red/20 hover:shadow-xl hover:shadow-red/30 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-light disabled:opacity-50 transition-all duration-300\">\n                    {{ __('Log in') }}\n                </button>\n            </div>\n\n            <div class=\"flex justify-center items-center mt-6 gap-3 text-sm\">\n                @if (Route::has('password.request'))\n                    <a class=\"text-base-300 hover:text-red transition-colors duration-200 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red focus:ring-offset-base-900 rounded-md\"\n                        href=\"{{ route('password.request') }}\">\n                        {{ __('Forgot password?') }}\n                    </a>\n                @endif\n\n                @if (!ce())\n                    <span class=\"text-base-700\">|</span>\n                    <a class=\"text-base-300 hover:text-red transition-colors duration-200 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red focus:ring-offset-base-900 rounded-md\"\n                        href=\"{{ route('register') }}\">\n                        {{ __('Create account') }}\n                    </a>\n                @endif\n            </div>\n\n            @if (config('services.google.enabled'))\n                <div class=\"mt-6\">\n                    <div class=\"relative\">\n                        <div class=\"absolute inset-0 flex items-center\">\n                            <div class=\"w-full border-t border-base-700\"></div>\n                        </div>\n                        <div class=\"relative flex justify-center text-sm\">\n                            <span class=\"px-2 bg-base-900 text-base-400\">Or continue with</span>\n                        </div>\n                    </div>\n                    <div class=\"mt-6\">\n                        <a href=\"{{ route('login.socialite', ['provider' => 'google']) }}\"\n                            class=\"flex items-center justify-center gap-2 w-full py-2.5 px-4 rounded-lg bg-base-850 border border-base-700 text-base-100 hover:bg-base-800 hover:border-base-600 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red focus:ring-offset-base-900 transition-all duration-200\">\n                            @svg('tni-google-o', 'size-6')\n                            <span>Sign in with Google</span>\n                        </a>\n                    </div>\n                </div>\n            @endif\n        </form>\n    </x-authentication-card>\n</x-guest-layout>\n"
  },
  {
    "path": "resources/views/auth/register.blade.php",
    "content": "<x-guest-layout>\n    <x-authentication-card>\n        <x-slot name=\"logo\">\n            <x-authentication-card-logo />\n        </x-slot>\n\n        <!-- Welcome header with fade-in animation -->\n        <div class=\"mb-8 text-center opacity-0 translate-y-4 animate-[fadeInUp_0.5s_ease-out_0.05s_forwards]\">\n            <h2 class=\"text-2xl font-semibold bg-gradient-to-r from-base-50 via-base-100 to-base-200 bg-clip-text text-transparent\">\n                {{ __('Create your account') }}\n            </h2>\n            <p class=\"mt-2 text-sm text-base-400\">{{ __('Join us and start monitoring today') }}</p>\n        </div>\n\n        <x-validation-errors class=\"mb-6\" />\n\n        <form method=\"POST\" action=\"{{ route('register') }}\" x-data=\"{\n            step: 0\n        }\" x-init=\"setTimeout(() => step = 1, 80)\">\n            @csrf\n\n            <!-- Name field - Staggered fade in -->\n            <div class=\"opacity-0 translate-y-4 transition-all duration-400\"\n                :class=\"step >= 1 && 'opacity-100 translate-y-0'\"\n                x-init=\"setTimeout(() => step = 2, 160)\">\n                <div class=\"relative group\">\n                    <x-label for=\"name\" value=\"{{ __('Name') }}\" class=\"transition-colors duration-150 group-focus-within:text-red\" />\n                    <div class=\"relative mt-1\">\n                        <x-input id=\"name\" class=\"block w-full transition-all duration-200 hover:ring-2 hover:ring-red/20 focus-within:scale-[1.01]\" \n                            type=\"text\" name=\"name\" :value=\"old('name')\" required\n                            autofocus autocomplete=\"name\" />\n                        <!-- Animated check icon when field has value -->\n                        <div class=\"absolute right-3 top-1/2 -translate-y-1/2 opacity-0 scale-0 transition-all duration-200\"\n                            x-data=\"{ show: false }\"\n                            x-init=\"$el.previousElementSibling.addEventListener('input', (e) => show = e.target.value.length > 0)\"\n                            :class=\"show && 'opacity-100 scale-100'\">\n                            <svg class=\"w-5 h-5 text-green\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                                <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\"></path>\n                            </svg>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <!-- Email field - Staggered fade in -->\n            <div class=\"mt-6 opacity-0 translate-y-4 transition-all duration-400\"\n                :class=\"step >= 2 && 'opacity-100 translate-y-0'\"\n                x-init=\"setTimeout(() => step = 3, 320)\">\n                <div class=\"relative group\">\n                    <x-label for=\"email\" value=\"{{ __('Email') }}\" class=\"transition-colors duration-150 group-focus-within:text-red\" />\n                    <div class=\"relative mt-1\">\n                        <x-input id=\"email\" class=\"block w-full transition-all duration-200 hover:ring-2 hover:ring-red/20 focus-within:scale-[1.01]\" \n                            type=\"email\" name=\"email\" :value=\"old('email')\"\n                            required autocomplete=\"username\" />\n                        <!-- Animated check icon when valid email -->\n                        <div class=\"absolute right-3 top-1/2 -translate-y-1/2 opacity-0 scale-0 transition-all duration-200\"\n                            x-data=\"{ show: false }\"\n                            x-init=\"$el.previousElementSibling.addEventListener('input', (e) => show = e.target.value.match(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/))\"\n                            :class=\"show && 'opacity-100 scale-100'\">\n                            <svg class=\"w-5 h-5 text-green\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                                <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\"></path>\n                            </svg>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <!-- Password group with strength indicator -->\n            <div class=\"mt-6 opacity-0 translate-y-4 transition-all duration-400\"\n                :class=\"step >= 3 && 'opacity-100 translate-y-0'\"\n                x-init=\"setTimeout(() => step = 4, 480)\">\n                <x-password-group password-id=\"password\"\n                    password-confirmation-id=\"password_confirmation\"\n                    password-name=\"password\"\n                    class=\"space-y-6\" />\n            </div>\n\n            <div class=\"hidden\" x-init=\"setTimeout(() => step = 5, 640)\"></div>\n\n            <!-- Terms checkbox - Staggered fade in -->\n            @if (Laravel\\Jetstream\\Jetstream::hasTermsAndPrivacyPolicyFeature())\n                <div class=\"mt-6 opacity-0 translate-y-4 transition-all duration-400\"\n                    :class=\"step >= 5 && 'opacity-100 translate-y-0'\">\n                    <div class=\"relative group\">\n                        <x-label for=\"terms\">\n                            <div class=\"flex items-start p-4 rounded-lg border border-base-700 bg-base-850/50 transition-all duration-200 hover:border-base-600 hover:bg-base-850\">\n                                <x-checkbox name=\"terms\" id=\"terms\" required class=\"mt-0.5 transition-transform duration-150 hover:scale-110\" />\n\n                                <div class=\"ms-3 text-sm text-base-300\">\n                                    {!! __('I agree to the :terms_of_service and :privacy_policy', [\n                                        'terms_of_service' =>\n                                            '<a target=\"_blank\" href=\"' .\n                                            route('terms.show') .\n                                            '\" class=\"text-base-200 hover:text-red transition-colors duration-150 rounded-md focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red focus:ring-offset-base-900 inline-flex items-center gap-1 group/link\">' .\n                                            __('Terms of Service') .\n                                            '<svg class=\"w-3 h-3 opacity-0 -translate-x-1 transition-all duration-150 group-hover/link:opacity-100 group-hover/link:translate-x-0\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\"></path></svg>' .\n                                            '</a>',\n                                        'privacy_policy' =>\n                                            '<a target=\"_blank\" href=\"' .\n                                            route('policy.show') .\n                                            '\" class=\"text-base-200 hover:text-red transition-colors duration-150 rounded-md focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red focus:ring-offset-base-900 inline-flex items-center gap-1 group/link\">' .\n                                            __('Privacy Policy') .\n                                            '<svg class=\"w-3 h-3 opacity-0 -translate-x-1 transition-all duration-150 group-hover/link:opacity-100 group-hover/link:translate-x-0\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\"></path></svg>' .\n                                            '</a>',\n                                    ]) !!}\n                                </div>\n                            </div>\n                        </x-label>\n                    </div>\n                </div>\n            @else\n                <!-- If no terms, show button earlier -->\n                <div x-init=\"setTimeout(() => step = 5, 640)\" class=\"hidden\"></div>\n            @endif\n\n            <!-- Submit button with pulsing attention grabber - Staggered fade in -->\n            <div class=\"mt-8 opacity-0 translate-y-4 transition-all duration-400\"\n                :class=\"step >= 5 && 'opacity-100 translate-y-0'\"\n                x-init=\"setTimeout(() => step = 6, 960)\">\n                <button type=\"submit\"\n                    class=\"group relative w-full inline-flex items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-red via-orange to-red bg-300% animate-gradient-shift px-6 py-3 text-sm font-semibold text-base-100 shadow-lg shadow-red/20 hover:shadow-2xl hover:shadow-red/40 hover:scale-[1.02] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-light disabled:opacity-50 transition-all duration-250 overflow-hidden\">\n                    <!-- Shimmer effect on hover -->\n                    <div class=\"absolute inset-0 -translate-x-full group-hover:translate-x-full transition-transform duration-800 bg-gradient-to-r from-transparent via-white/20 to-transparent\"></div>\n                    <span class=\"relative\">{{ __('Create Account') }}</span>\n                    <svg class=\"relative w-5 h-5 transition-transform duration-200 group-hover:translate-x-1\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 7l5 5m0 0l-5 5m5-5H6\"></path>\n                    </svg>\n                </button>\n            </div>\n\n            <!-- Already registered link - Staggered fade in -->\n            <div class=\"flex justify-center items-center mt-6 text-sm opacity-0 transition-all duration-400\"\n                :class=\"step >= 6 && 'opacity-100'\">\n                <a class=\"text-base-300 hover:text-red transition-all duration-150 rounded-md focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red focus:ring-offset-base-900 inline-flex items-center gap-2 group/login\"\n                    href=\"{{ route('login') }}\">\n                    <svg class=\"w-4 h-4 transition-transform duration-150 group-hover/login:-translate-x-1\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 19l-7-7m0 0l7-7m-7 7h18\"></path>\n                    </svg>\n                    {{ __('Already registered? Sign in') }}\n                </a>\n            </div>\n\n            @if (config('services.google.enabled'))\n                <div class=\"mt-8 opacity-0 transition-all duration-400\"\n                    :class=\"step >= 6 && 'opacity-100'\">\n                    <div class=\"relative\">\n                        <div class=\"absolute inset-0 flex items-center\">\n                            <div class=\"w-full border-t border-base-700\"></div>\n                        </div>\n                        <div class=\"relative flex justify-center text-sm\">\n                            <span class=\"px-3 bg-base-900 text-base-400\">Or continue with</span>\n                        </div>\n                    </div>\n                    <div class=\"mt-6\">\n                        <a href=\"{{ route('login.socialite', ['provider' => 'google']) }}\"\n                            class=\"group flex items-center justify-center gap-3 w-full py-2.5 px-4 rounded-lg bg-base-850 border border-base-700 text-base-100 hover:bg-base-800 hover:border-base-600 hover:scale-[1.01] focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red focus:ring-offset-base-900 transition-all duration-250\">\n                            <span class=\"transition-transform duration-250 group-hover:rotate-[360deg]\">\n                                @svg('tni-google-o', 'size-6')\n                            </span>\n                            <span>Sign in with Google</span>\n                        </a>\n                    </div>\n                </div>\n            @endif\n        </form>\n    </x-authentication-card>\n</x-guest-layout>\n"
  },
  {
    "path": "resources/views/auth/reset-password.blade.php",
    "content": "<x-guest-layout>\n    <x-authentication-card>\n        <x-slot name=\"logo\">\n            <x-authentication-card-logo />\n        </x-slot>\n\n        <x-validation-errors class=\"mb-6\" />\n\n        <form method=\"POST\" action=\"{{ route('password.update') }}\">\n            @csrf\n\n            <input type=\"hidden\" name=\"token\" value=\"{{ $request->route('token') }}\">\n\n            <div class=\"block\">\n                <x-label for=\"email\" value=\"{{ __('Email') }}\" />\n                <x-input id=\"email\" class=\"block mt-1 w-full\" type=\"email\" name=\"email\" :value=\"old('email', $request->email)\" required autofocus autocomplete=\"username\" />\n            </div>\n\n            <div class=\"mt-4\">\n                <x-label for=\"password\" value=\"{{ __('Password') }}\" />\n                <x-input id=\"password\" class=\"block mt-1 w-full\" type=\"password\" name=\"password\" required autocomplete=\"new-password\" />\n            </div>\n\n            <div class=\"mt-4\">\n                <x-label for=\"password_confirmation\" value=\"{{ __('Confirm Password') }}\" />\n                <x-input id=\"password_confirmation\" class=\"block mt-1 w-full\" type=\"password\" name=\"password_confirmation\" required autocomplete=\"new-password\" />\n            </div>\n\n            <div class=\"mt-6\">\n                <button type=\"submit\"\n                    class=\"w-full inline-flex items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-red via-orange to-red bg-300% animate-gradient-shift px-6 py-2.5 text-sm font-semibold text-base-100 shadow-lg shadow-red/20 hover:shadow-xl hover:shadow-red/30 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-light disabled:opacity-50 transition-all duration-300\">\n                    {{ __('Reset Password') }}\n                </button>\n            </div>\n        </form>\n    </x-authentication-card>\n</x-guest-layout>\n"
  },
  {
    "path": "resources/views/auth/two-factor-challenge.blade.php",
    "content": "<x-guest-layout>\n    <x-authentication-card>\n        <x-slot name=\"logo\">\n            <x-authentication-card-logo />\n        </x-slot>\n\n        <div x-data=\"{ recovery: false }\">\n            <div class=\"mb-6 text-sm text-base-300 leading-relaxed\" x-show=\"! recovery\">\n                {{ __('Please confirm access to your account by entering the authentication code provided by your authenticator application.') }}\n            </div>\n\n            <div class=\"mb-6 text-sm text-base-300 leading-relaxed\" x-cloak x-show=\"recovery\">\n                {{ __('Please confirm access to your account by entering one of your emergency recovery codes.') }}\n            </div>\n\n            <x-validation-errors class=\"mb-6\" />\n\n            <form method=\"POST\" action=\"{{ route('two-factor.login') }}\">\n                @csrf\n\n                <div class=\"mt-4\" x-show=\"! recovery\">\n                    <x-label for=\"code\" value=\"{{ __('Code') }}\" />\n                    <x-input id=\"code\" class=\"block mt-1 w-full\" type=\"text\" inputmode=\"numeric\" name=\"code\" autofocus x-ref=\"code\" autocomplete=\"one-time-code\" />\n                </div>\n\n                <div class=\"mt-4\" x-cloak x-show=\"recovery\">\n                    <x-label for=\"recovery_code\" value=\"{{ __('Recovery Code') }}\" />\n                    <x-input id=\"recovery_code\" class=\"block mt-1 w-full\" type=\"text\" name=\"recovery_code\" x-ref=\"recovery_code\" autocomplete=\"one-time-code\" />\n                </div>\n\n                <div class=\"flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4 mt-6\">\n                    <button type=\"button\" class=\"text-sm text-base-300 hover:text-red transition-colors duration-200 cursor-pointer order-2 sm:order-1\"\n                                    x-show=\"! recovery\"\n                                    x-on:click=\"\n                                        recovery = true;\n                                        $nextTick(() => { $refs.recovery_code.focus() })\n                                    \">\n                        {{ __('Use a recovery code') }}\n                    </button>\n\n                    <button type=\"button\" class=\"text-sm text-base-300 hover:text-red transition-colors duration-200 cursor-pointer order-2 sm:order-1\"\n                                    x-cloak\n                                    x-show=\"recovery\"\n                                    x-on:click=\"\n                                        recovery = false;\n                                        $nextTick(() => { $refs.code.focus() })\n                                    \">\n                        {{ __('Use an authentication code') }}\n                    </button>\n\n                    <button type=\"submit\"\n                        class=\"w-full sm:w-auto inline-flex items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-red via-orange to-red bg-300% animate-gradient-shift px-6 py-2.5 text-sm font-semibold text-base-100 shadow-lg shadow-red/20 hover:shadow-xl hover:shadow-red/30 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-light disabled:opacity-50 transition-all duration-300 order-1 sm:order-2\">\n                        {{ __('Log in') }}\n                    </button>\n                </div>\n            </form>\n        </div>\n    </x-authentication-card>\n</x-guest-layout>\n"
  },
  {
    "path": "resources/views/auth/verify-email.blade.php",
    "content": "<x-guest-layout>\n    <x-authentication-card>\n        <x-slot name=\"logo\">\n            <x-authentication-card-logo />\n        </x-slot>\n\n        <div class=\"mb-6 text-sm text-base-300 leading-relaxed\">\n            {{ __('Before continuing, could you verify your email address by clicking on the link we just emailed to you? If you didn\\'t receive the email, we will gladly send you another.') }}\n        </div>\n\n        @if (session('status') == 'verification-link-sent')\n            <div class=\"mb-6 font-medium text-sm text-green-light bg-green/10 border border-green/30 rounded-lg px-4 py-3\">\n                {{ __('A new verification link has been sent to the email address you provided in your profile settings.') }}\n            </div>\n        @endif\n\n        <div class=\"mt-6 flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4\">\n            <form method=\"POST\" action=\"{{ route('verification.send') }}\" class=\"flex-1\">\n                @csrf\n\n                <button type=\"submit\"\n                    class=\"w-full inline-flex items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-red via-orange to-red bg-300% animate-gradient-shift px-6 py-2.5 text-sm font-semibold text-base-100 shadow-lg shadow-red/20 hover:shadow-xl hover:shadow-red/30 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-light disabled:opacity-50 transition-all duration-300\">\n                    {{ __('Resend Verification Email') }}\n                </button>\n            </form>\n\n            <form method=\"POST\" action=\"{{ route('logout') }}\" class=\"flex-1\">\n                @csrf\n\n                <button type=\"submit\"\n                    class=\"w-full inline-flex items-center justify-center gap-2 rounded-lg bg-base-850 border border-base-700 px-6 py-2.5 text-sm font-semibold text-base-100 hover:bg-base-800 hover:border-base-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red transition-all duration-200\">\n                    {{ __('Log Out') }}\n                </button>\n            </form>\n        </div>\n    </x-authentication-card>\n</x-guest-layout>\n"
  },
  {
    "path": "resources/views/components/action-message.blade.php",
    "content": "@props(['on'])\n\n<div x-data=\"{ shown: false, timeout: null }\"\n    x-init=\"@this.on('{{ $on }}', () => { clearTimeout(timeout); shown = true; timeout = setTimeout(() => { shown = false }, 2000); })\"\n    x-show.transition.out.opacity.duration.1500ms=\"shown\"\n    x-transition:leave.opacity.duration.1500ms\n    style=\"display: none;\"\n    {{ $attributes->merge(['class' => 'text-sm text-gray-600']) }}>\n    {{ $slot->isEmpty() ? 'Saved.' : $slot }}\n</div>\n"
  },
  {
    "path": "resources/views/components/action-section.blade.php",
    "content": "<div {{ $attributes->merge(['class' => 'md:grid md:grid-cols-3 md:gap-8']) }}>\n    <x-section-title>\n        <x-slot name=\"title\">{{ $title }}</x-slot>\n        <x-slot name=\"description\">{{ $description }}</x-slot>\n    </x-section-title>\n\n    <div class=\"mt-5 md:mt-0 md:col-span-2\">\n        <div class=\"px-6 py-8 sm:p-8 bg-gradient-to-br from-base-850 to-base-900 border border-base-700 shadow-xl sm:rounded-xl relative overflow-hidden\">\n            <!-- Subtle gradient overlay -->\n            <div class=\"absolute inset-0 bg-gradient-to-b from-base-800/10 to-transparent pointer-events-none\"></div>\n            \n            <div class=\"relative\">\n                {{ $content }}\n            </div>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/alert.blade.php",
    "content": "<div x-data=\"{ currentAlert: {} }\"\n    @alert.window=\"currentAlert = $event.detail[0] ?? {}\" x-cloak class=\"mb-4\">\n\n{{--    Same alerts as the components just rendered via a JS event --}}\n    <div class=\"rounded-xl bg-gradient-to-r from-green to-green-light border border-green-light/30 p-5 shadow-lg shadow-green/20\" x-show=\"currentAlert.type == 'success'\">\n        <div class=\"flex\">\n            <div class=\"shrink-0\">\n                <x-heroicon-o-check-circle class=\"h-6 w-6 text-white\"/>\n            </div>\n            <div class=\"ml-3 flex-1\">\n                <div class=\"flex justify-between\">\n                    <h3 class=\"text-sm font-semibold text-white flex-1\" x-text=\"currentAlert.title\"></h3>\n                    <span x-on:click=\"currentAlert = {}\" class=\"hover:bg-white/20 rounded-lg p-1 transition-colors duration-200\">\n                        <x-tni-x-circle-o class=\"w-5 h-5 text-white cursor-pointer\"/>\n                    </span>\n                </div>\n                <div class=\"mt-2 text-sm text-base-50\">\n                    <p x-text=\"currentAlert.message\"></p>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div class=\"rounded-xl bg-gradient-to-r from-red to-red-light border border-red-light/30 p-5 shadow-lg shadow-red/20\" x-show=\"currentAlert.type == 'danger'\">\n        <div class=\"flex\">\n            <div class=\"shrink-0\">\n                <x-heroicon-o-exclamation-circle class=\"h-6 w-6 text-white\"/>\n            </div>\n            <div class=\"ml-3 flex-1\">\n                <div class=\"flex justify-between\">\n                    <h3 class=\"text-sm font-semibold text-white flex-1\" x-text=\"currentAlert.title\"></h3>\n                    <span x-on:click=\"currentAlert = {}\" class=\"hover:bg-white/20 rounded-lg p-1 transition-colors duration-200\">\n                        <x-tni-x-circle-o class=\"w-5 h-5 text-white cursor-pointer\"/>\n                    </span>\n                </div>\n                <div class=\"mt-2 text-sm text-base-50\">\n                    <p x-text=\"currentAlert.message\"></p>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div class=\"rounded-xl bg-gradient-to-r from-blue to-blue-light border border-blue-light/30 p-5 shadow-lg shadow-blue/20\" x-show=\"currentAlert.type == 'info'\">\n        <div class=\"flex\">\n            <div class=\"shrink-0\">\n                <x-tni-info-circle-o class=\"h-6 w-6 text-white\"/>\n            </div>\n            <div class=\"ml-3 flex-1\">\n                <div class=\"flex justify-between\">\n                    <h3 class=\"text-sm font-semibold text-white flex-1\" x-text=\"currentAlert.title\"></h3>\n                    <span x-on:click=\"currentAlert = {}\" class=\"hover:bg-white/20 rounded-lg p-1 transition-colors duration-200\">\n                        <x-tni-x-circle-o class=\"w-5 h-5 text-white cursor-pointer\"/>\n                    </span>\n                </div>\n                <div class=\"mt-2 text-sm text-base-50\">\n                    <p x-text=\"currentAlert.message\"></p>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div class=\"rounded-xl bg-gradient-to-r from-yellow to-yellow-light border border-yellow-light/30 p-5 shadow-lg shadow-yellow/20\" x-show=\"currentAlert.type == 'warning'\">\n        <div class=\"flex\">\n            <div class=\"shrink-0\">\n                <x-heroicon-o-exclamation-triangle class=\"h-6 w-6 text-white\"/>\n            </div>\n            <div class=\"ml-3 flex-1\">\n                <div class=\"flex justify-between\">\n                    <h3 class=\"text-sm font-semibold text-white flex-1\" x-text=\"currentAlert.title\"></h3>\n                    <span x-on:click=\"currentAlert = {}\" class=\"hover:bg-white/20 rounded-lg p-1 transition-colors duration-200\">\n                        <x-tni-x-circle-o class=\"w-5 h-5 text-white cursor-pointer\"/>\n                    </span>\n                </div>\n                <div class=\"mt-2 text-sm text-base-50\">\n                    <p x-text=\"currentAlert.message\"></p>\n                </div>\n            </div>\n        </div>\n    </div>\n\n\n    @if (session('alert'))\n        @php($type = session('alert-type'))\n\n        <x-dynamic-component :component=\"$type->component()\"\n                             :title=\"session('alert-title')\"\n                             :message=\"session('alert-message')\"\n        />\n    @endif\n    </div>\n"
  },
  {
    "path": "resources/views/components/alerts/danger.blade.php",
    "content": "<div class=\"rounded-xl bg-gradient-to-r from-red to-red-light border border-red-light/30 p-5 shadow-lg shadow-red/20\" \n     x-data=\"{ show: true }\" \n     x-show=\"show\"\n     x-transition:enter=\"transition ease-out duration-300\"\n     x-transition:enter-start=\"opacity-0 translate-y-2\"\n     x-transition:enter-end=\"opacity-100 translate-y-0\"\n     x-transition:leave=\"transition ease-in duration-200\"\n     x-transition:leave-start=\"opacity-100\"\n     x-transition:leave-end=\"opacity-0\">\n    <div class=\"flex\">\n        <div class=\"shrink-0\">\n            <x-heroicon-o-exclamation-circle class=\"h-6 w-6 text-white\" />\n        </div>\n        <div class=\"ml-3 flex-1\">\n            <div class=\"flex justify-between\">\n                <h3 class=\"text-sm font-semibold text-white flex-1\">{{ $title }}</h3>\n                <button x-on:click=\"show = false\" class=\"hover:bg-white/20 rounded-lg p-1 transition-colors duration-200\">\n                    <x-tni-x-circle-o class=\"w-5 h-5 text-white cursor-pointer\" />\n                </button>\n            </div>\n            @if (!blank($message ?? ''))\n                <div class=\"mt-2 text-sm text-base-50\">\n                    <p>{{ $message }}</p>\n                </div>\n            @endif\n            <div class=\"mt-2 text-sm text-base-50\">\n                {{ $slot }}\n            </div>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/alerts/info.blade.php",
    "content": "<div class=\"rounded-xl bg-gradient-to-r from-blue to-blue-light border border-blue-light/30 p-5 shadow-lg shadow-blue/20\" \n     x-data=\"{ show: true }\" \n     x-show=\"show\"\n     x-transition:enter=\"transition ease-out duration-300\"\n     x-transition:enter-start=\"opacity-0 translate-y-2\"\n     x-transition:enter-end=\"opacity-100 translate-y-0\"\n     x-transition:leave=\"transition ease-in duration-200\"\n     x-transition:leave-start=\"opacity-100\"\n     x-transition:leave-end=\"opacity-0\">\n    <div class=\"flex\">\n        <div class=\"shrink-0\">\n            <x-tni-info-circle-o class=\"h-6 w-6 text-white\" />\n        </div>\n        <div class=\"ml-3 flex-1\">\n            <div class=\"flex justify-between\">\n                <h3 class=\"text-sm font-semibold text-white flex-1\">{{ $title }}</h3>\n                <button x-on:click=\"show = false\" class=\"hover:bg-white/20 rounded-lg p-1 transition-colors duration-200\">\n                    <x-tni-x-circle-o class=\"w-5 h-5 text-white cursor-pointer\" />\n                </button>\n            </div>\n            @if (!blank($message ?? ''))\n                <div class=\"mt-2 text-sm text-base-50\">\n                    <p>{{ $message }}</p>\n                </div>\n            @endif\n            <div class=\"mt-2 text-sm text-base-50\">\n                {{ $slot }}\n            </div>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/alerts/success.blade.php",
    "content": "<div class=\"rounded-xl bg-gradient-to-r from-green to-green-light border border-green-light/30 p-5 shadow-lg shadow-green/20\" \n     x-data=\"{ show: true }\" \n     x-show=\"show\"\n     x-transition:enter=\"transition ease-out duration-300\"\n     x-transition:enter-start=\"opacity-0 translate-y-2\"\n     x-transition:enter-end=\"opacity-100 translate-y-0\"\n     x-transition:leave=\"transition ease-in duration-200\"\n     x-transition:leave-start=\"opacity-100\"\n     x-transition:leave-end=\"opacity-0\">\n    <div class=\"flex\">\n        <div class=\"shrink-0\">\n            <x-heroicon-o-check-circle class=\"h-6 w-6 text-white\" />\n        </div>\n        <div class=\"ml-3 flex-1\">\n            <div class=\"flex justify-between\">\n                <h3 class=\"text-sm font-semibold text-white flex-1\">{{ $title }}</h3>\n                <button x-on:click=\"show = false\" class=\"hover:bg-white/20 rounded-lg p-1 transition-colors duration-200\">\n                    <x-tni-x-circle-o class=\"w-5 h-5 text-white cursor-pointer\" />\n                </button>\n            </div>\n            @if (!blank($message ?? ''))\n                <div class=\"mt-2 text-sm text-base-50\">\n                    <p>{{ $message }}</p>\n                </div>\n            @endif\n            <div class=\"mt-2 text-sm text-base-50\">\n                {{ $slot }}\n            </div>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/alerts/warning.blade.php",
    "content": "<div class=\"rounded-xl bg-gradient-to-r from-yellow to-yellow-light border border-yellow-light/30 p-5 shadow-lg shadow-yellow/20\" \n     x-data=\"{ show: true }\" \n     x-show=\"show\"\n     x-transition:enter=\"transition ease-out duration-300\"\n     x-transition:enter-start=\"opacity-0 translate-y-2\"\n     x-transition:enter-end=\"opacity-100 translate-y-0\"\n     x-transition:leave=\"transition ease-in duration-200\"\n     x-transition:leave-start=\"opacity-100\"\n     x-transition:leave-end=\"opacity-0\">\n    <div class=\"flex\">\n        <div class=\"shrink-0\">\n            <x-heroicon-o-exclamation-triangle class=\"h-6 w-6 text-white\" />\n        </div>\n        <div class=\"ml-3 flex-1\">\n            <div class=\"flex justify-between\">\n                <h3 class=\"text-sm font-semibold text-white flex-1\">{{ $title }}</h3>\n                <button x-on:click=\"show = false\" class=\"hover:bg-white/20 rounded-lg p-1 transition-colors duration-200\">\n                    <x-tni-x-circle-o class=\"w-5 h-5 text-white cursor-pointer\" />\n                </button>\n            </div>\n            @if (!blank($message ?? ''))\n                <div class=\"mt-2 text-sm text-base-50\">\n                    <p>{{ $message }}</p>\n                </div>\n            @endif\n            <div class=\"mt-2 text-sm text-base-50\">\n                {{ $slot }}\n            </div>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/application-logo.blade.php",
    "content": "<svg version=\"1.2\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1388 893\" {{ $attributes }}>\n    <style>\n        .red { fill: #d14d41 }\n        .white { fill: #E6E4D9 }\n    </style>\n    <g>\n        <g>\n            <path class=\"white\" d=\"m231.9 718.5l-98.6 158.5q-10.3 16.9-18.7 16.9-8.1 0-18.4-16.9l-96.5-158.5h38.6l77.1 128.2 77.8-128.2z\"/>\n            <path class=\"red\" d=\"m281.4 891.1h-30v-172.6h30z\"/>\n            <path class=\"red\" d=\"m514.7 891.1h-109.7q-37.6 0-62.3-25-24.6-25-24.6-63.2 0-37.9 24.4-61.1 24.4-23.3 62.5-23.3h97.6v30.1h-97.6q-24.7 0-40.8 15.9-16 15.9-16 41 0 24.7 16 40.2 16.1 15.4 40.8 15.4h79.6v-37.7h-81.9v-27.5h112z\"/>\n            <path class=\"red\" d=\"m576.9 891.1h-30.1v-172.6h30.1z\"/>\n            <path class=\"red\" d=\"m758.8 891.1h-148.8v-172.6h30v142.6h118.8z\"/>\n            <path class=\"red\" d=\"m1003.2 891.1l-38.6-0.2-29.1-48.3h-81.9l18.2-29.8h45.7l-30-49.8-77.9 127.9h-38.6l93.7-155.9q3.6-6.1 9.9-11.2 7.6-5.8 13.6-5.8 6.6 0 13.7 5.6 6 4.8 9.8 11.4z\"/>\n            <path class=\"red\" d=\"m1190 876.2q0 16.7-11.4 16.7-8.3 0-18.7-10.3l-110.7-111.3v119.8h-29.8v-157.7q0-6 3.3-10.4 3.3-4.5 9.3-4.5 8.4 0 17.2 8.9l110.7 110.9v-119.8h30.1z\"/>\n            <path class=\"red\" d=\"m1386.3 748.6h-67.9v142.5h-29.9v-142.5h-68.2v-30.1h166z\"/>\n            <path fill-rule=\"evenodd\" class=\"red\" d=\"m482.4 243.2c0-1.2-0.2-2.3 0-3.5 1.2-10.1 2.5-20.2 3.7-30.3 1.8-14 3.4-28 5.2-42 2-16.4 3.9-32.8 6.2-49.1 2.6-18.2 5.5-36.3 8.3-54.5 1.6-10.3 3.2-20.7 5-31 3-17.6 18.1-30.6 36-32.2 13.2-1.2 26 1 38.8 3.1 34.5 5.8 67.6 16.2 99.6 30.2 2 0.8 4 1.6 6 2.4 2.2 1 4.5 1.9 6.9 2.8 5-2.1 9.9-4.5 15-6.5 16.3-6.4 32.5-13.1 49-18.8 17-5.8 34.5-10 52.4-12.2 6-0.7 12-1.5 18-1.3 11.9 0.3 23.1 3.2 31.7 12.4 5.7 6.1 9.3 13.1 10.6 21.3 2.5 16.3 5.3 32.5 7.8 48.8q3.5 23 6.7 46.1c2.2 16 4.3 31.9 6.3 47.9 2.3 19.4 4.4 38.9 6.6 58.4 0.3 2.6 0.7 5.2 1 7.9l-0.9 0.1q-1.1-5.6-2.3-11.2c-3.2-1-403.2-1.6-414.5-0.5-0.9 3.8-1.7 7.7-2.6 11.7q-0.3 0-0.5 0zm398.8-93.1c-0.1-1.9-0.1-3.4-0.3-4.8q-2.6-18.5-5.4-37-2.8-19.1-5.8-38.2-2.3-14.2-4.7-28.5c-0.9-5.3-1.5-10.6-4.5-15.3-6.6-10.1-16.2-14.1-27.7-14.8-9.5-0.5-18.7 1.6-27.9 3.4-25.3 5-49.9 12.8-73.7 22.7-14.8 6.1-29.3 12.8-43.9 19.2-0.8 0.3-1.5 0.9-2.4 1.5q15.9 7.3 30.7 14.2v9.7c-2.2-0.8-4-1.5-5.6-2.3-15.2-7-30.2-14.1-45.4-21.1-23-10.6-46.1-21.3-69.2-31.8-10.8-4.8-21.8-9.2-32.8-13.4-6.7-2.6-13.6-2.8-20.6-0.6-10.9 3.5-18 10.4-20.3 21.8-1.3 6.8-2.3 13.7-3.4 20.6q-3.1 19-6.1 38.1-2.4 15.2-4.7 30.5c-1.3 8.6-2.6 17.2-3.8 26.1zm-196.6-103.4c-10.5-7.2-78.1-29-85.4-27.8 24.6 11.1 49.6 22.4 74.4 33.6 3.8-2 7.3-3.9 11-5.8z\"/>\n            <path fill-rule=\"evenodd\" class=\"red\" d=\"m485.9 328.3c-8.5-2.1-16.5-3.9-24.6-6-32.5-8-63.9-18.9-93.6-34.4-13.7-7.2-26.9-15.3-37.3-27.2-4.9-5.6-9.3-11.5-11.1-18.9-3.2-12.7-0.7-24.2 7.5-34.5 8.1-10 18.4-17.2 29.6-23.3 17.1-9.3 35.3-15.7 54.1-20.7 20.6-5.6 41.5-9.5 62.6-12.6 4.2-0.6 8.5-0.8 13.3-1.2-0.6 4.4-1.2 8-1.6 11.1-12.1 2-23.8 3.6-35.4 5.9-28.3 5.6-56.3 12.3-82.4 25.3-10.4 5.1-20.5 10.7-28.6 19.3-5.1 5.5-8.6 11.8-9.1 19.3-0.2 3.6 0.7 7.6 2.1 10.9 1.7 4 4.3 7.8 7.1 11.1 9.6 11.3 22.3 18.5 35.3 25.2 19.8 10.1 40.2 18.7 61.6 24.7 18.9 5.2 37.9 10 57 14.2 17 3.7 34.3 6.2 51.5 9.1 8.4 1.4 16.9 2.8 25.4 3.8 8.2 1.1 16.5 1.8 24.7 2.6 12.2 1.1 24.3 2.4 36.5 3.2 12.1 0.7 24.2 1 36.3 1.3 10.2 0.3 20.4 0.5 30.6 0.4 23.3-0.3 46.5-0.8 69.7-2.7 13.1-1.1 26.2-2.3 39.2-3.9 26-3.1 51.8-7 77.5-12.2 29.9-6 59.7-13 88.5-23.5 20.5-7.4 40.5-15.9 58.8-28.1 7.6-5.1 14.7-11 19.9-18.7 8.1-11.8 7.4-23.3-1.8-34.4-5-5.9-11.2-10.4-17.8-14.4-15.4-9.4-31.9-16-49.1-21.1-19.3-5.8-39-10.1-58.8-13.5-7.4-1.2-14.8-2.2-22.8-3.4-0.6-3.2-1.2-6.7-1.9-10.7 2.9 0 5.5-0.3 8 0.1 11.4 1.7 22.8 3.2 34.1 5.5 14.3 2.9 28.6 6 42.6 9.8 21.9 6.1 42.8 14.7 61.3 28.1 9.4 6.8 17.3 15 21.2 26.3 4.1 12 1.9 23.3-5.1 33.7-7.3 10.6-17.1 18.6-27.9 25.3-19.6 12.3-40.8 21.2-62.5 28.9-22.7 8.1-46.1 14-69.7 19.2-1.3 0.3-2.6 0.6-4.3 1-1.3 6.2-2.7 12.5-4 18.7-4.8 23.3-9.6 46.7-14.3 70-5 24.4-9.8 48.8-14.7 73.2-3 14.8-5.9 29.6-9.1 44.4-0.5 2.3-2.1 4.7-3.7 6.5-10.1 11.4-20.3 22.6-30.6 33.8-16.6 18.2-33.1 36.4-49.8 54.5-12.3 13.4-24.7 26.6-37.2 40h-84.4c-10.3-11.1-20.8-22.3-31.2-33.5q-30.9-33.6-61.7-67.2c-7.9-8.6-15.6-17.4-23.7-25.8-3.6-3.8-5.5-8.1-6.4-13.1-4.4-22.7-9-45.3-13.5-67.9q-5-25.6-10.1-51.2-6.2-31.2-12.3-62.3c-1.3-6.6-2.6-13.2-3.9-20zm35.5 123.2c36.1 33.8 72.5 67.9 108.9 101.9 15-5.7 29.7-11.3 44.8-17 3.4 3.5 6.8 7.1 10.4 10.9v-38.9h-20.8v-136.3c-0.7 0.4-1 0.5-1.2 0.7-0.7 0.8-1.4 1.6-2.2 2.4-9.6 10-20.4 18.3-33.6 22.9-15.9 5.7-32 7.3-48.7 3.5-16.4-3.8-31.1-11.1-43.9-21.9-10.3-8.8-20-18.3-30-27.5q-1.5-1.3-2.9-2.6-0.5 0.3-0.9 0.5c6.6 33.6 13.3 67.3 20.1 101.4zm183.3 84.8q23.7 8.8 46.7 17.4c6.1-5.7 11.8-11 17.6-16.3 14.7-13.5 29.4-27.1 44.1-40.7 1-0.9 1.8-2 2.5-3.2 8.6-14.6 17.4-29 25.6-43.8 10.6-18.9 20.6-38.1 30.9-57.1 0.5-1 0.8-2 1-3.1q2.7-13.6 5.3-27.2c0.8-3.9 1.5-7.9 2.2-11.9-2.7 1.5-4.7 3.2-6.7 5-8.8 7.9-17.2 16.3-26.4 23.7-21.5 17.2-45.8 26.7-73.9 24.1-17.9-1.7-33.8-8.5-47.4-20.2-4.2-3.5-7.8-7.6-12.4-12.1v137.8h-19.2v39.2c3.6-4.2 6.9-7.9 10.1-11.6zm36.9 108.1c0.6-0.5 0.8-0.6 1-0.8 24-26.2 48-52.4 71.9-78.6 9.3-10.2 18.4-20.7 27.5-31.1 1.3-1.5 2.4-3.4 2.8-5.3 3.5-17.1 6.9-34.3 10.2-51.5 0.6-2.6 1-5.3 1.5-8.2-43.8 40.5-87.1 80.6-130.3 120.6h-31.9c0.3 18.4-0.2 36.6 0.2 54.9zm-86.1-54.9c-43.3-40.4-86.6-80.7-130.5-121.6 0.2 1.9 0.2 2.9 0.4 3.8 3.2 16.3 6.6 32.5 9.5 48.8 1.4 7.4 4.1 13.6 9.3 19.2 18.2 19.5 36 39.5 54.1 59.2 13.9 15.2 28.1 30.4 41.9 45.3h45.4v-54.7zm57.9-239.6c3.6 7.3 7.6 13.6 12.6 19 20.1 22 45.2 28.8 73.9 23.2 16.3-3.2 30.7-11 43.1-21.7 13.7-11.7 26.6-24.3 39.8-36.6 0.6-0.5 1-1.3 1.4-1.9-28.3 4.1-56.4 8.3-84.5 12.3-28.4 4-56.9 5.4-86.3 5.7zm-47.1 1.1q-0.4-0.6-0.7-1.1c-56.2-0.4-111.5-8.5-166.7-18.4 0.9 1.6 1.9 2.9 3.1 4.1q6.5 6.4 13 12.7c11.4 11 22.8 22.2 36.1 30.9 20.8 13.7 43.2 18.6 67.6 11.8 16.3-4.7 29.5-14 39.6-27.7 2.9-3.9 5.3-8.2 8-12.3zm8.7 145.3h29.9v-145.8h-29.9zm68.2 64.3q-0.2-0.3-0.5-0.6h-48.1v19.1h28.5c6.8-6.2 13.4-12.3 20.1-18.5zm-84.5 18.3h26.9v-18.8h-47.5c7.3 6.7 14 12.8 20.6 18.8zm207.3-153.5l-0.4-0.1c-8.7 15.3-17.3 30.6-26.3 46.6 7.3-6.7 14.1-12.9 20.9-19.2q2.9-13.6 5.8-27.3zm-131.7 227.5h-39.4v6h33.7c1.8-1.9 3.4-3.6 5.7-6zm-86.4-0.1c2.3 2.5 3.9 4.4 5.3 6h32.3v-6z\"/>\n        </g>\n    </g>\n</svg>\n"
  },
  {
    "path": "resources/views/components/application-mark.blade.php",
    "content": "<svg version=\"1.2\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1388 893\" {{ $attributes }}>\n    <style>\n        .red { fill: #d14d41 }\n        .white { fill: #E6E4D9 }\n    </style>\n    <g>\n        <g>\n            <path class=\"white\" d=\"m231.9 718.5l-98.6 158.5q-10.3 16.9-18.7 16.9-8.1 0-18.4-16.9l-96.5-158.5h38.6l77.1 128.2 77.8-128.2z\"/>\n            <path class=\"red\" d=\"m281.4 891.1h-30v-172.6h30z\"/>\n            <path class=\"red\" d=\"m514.7 891.1h-109.7q-37.6 0-62.3-25-24.6-25-24.6-63.2 0-37.9 24.4-61.1 24.4-23.3 62.5-23.3h97.6v30.1h-97.6q-24.7 0-40.8 15.9-16 15.9-16 41 0 24.7 16 40.2 16.1 15.4 40.8 15.4h79.6v-37.7h-81.9v-27.5h112z\"/>\n            <path class=\"red\" d=\"m576.9 891.1h-30.1v-172.6h30.1z\"/>\n            <path class=\"red\" d=\"m758.8 891.1h-148.8v-172.6h30v142.6h118.8z\"/>\n            <path class=\"red\" d=\"m1003.2 891.1l-38.6-0.2-29.1-48.3h-81.9l18.2-29.8h45.7l-30-49.8-77.9 127.9h-38.6l93.7-155.9q3.6-6.1 9.9-11.2 7.6-5.8 13.6-5.8 6.6 0 13.7 5.6 6 4.8 9.8 11.4z\"/>\n            <path class=\"red\" d=\"m1190 876.2q0 16.7-11.4 16.7-8.3 0-18.7-10.3l-110.7-111.3v119.8h-29.8v-157.7q0-6 3.3-10.4 3.3-4.5 9.3-4.5 8.4 0 17.2 8.9l110.7 110.9v-119.8h30.1z\"/>\n            <path class=\"red\" d=\"m1386.3 748.6h-67.9v142.5h-29.9v-142.5h-68.2v-30.1h166z\"/>\n            <path fill-rule=\"evenodd\" class=\"red\" d=\"m482.4 243.2c0-1.2-0.2-2.3 0-3.5 1.2-10.1 2.5-20.2 3.7-30.3 1.8-14 3.4-28 5.2-42 2-16.4 3.9-32.8 6.2-49.1 2.6-18.2 5.5-36.3 8.3-54.5 1.6-10.3 3.2-20.7 5-31 3-17.6 18.1-30.6 36-32.2 13.2-1.2 26 1 38.8 3.1 34.5 5.8 67.6 16.2 99.6 30.2 2 0.8 4 1.6 6 2.4 2.2 1 4.5 1.9 6.9 2.8 5-2.1 9.9-4.5 15-6.5 16.3-6.4 32.5-13.1 49-18.8 17-5.8 34.5-10 52.4-12.2 6-0.7 12-1.5 18-1.3 11.9 0.3 23.1 3.2 31.7 12.4 5.7 6.1 9.3 13.1 10.6 21.3 2.5 16.3 5.3 32.5 7.8 48.8q3.5 23 6.7 46.1c2.2 16 4.3 31.9 6.3 47.9 2.3 19.4 4.4 38.9 6.6 58.4 0.3 2.6 0.7 5.2 1 7.9l-0.9 0.1q-1.1-5.6-2.3-11.2c-3.2-1-403.2-1.6-414.5-0.5-0.9 3.8-1.7 7.7-2.6 11.7q-0.3 0-0.5 0zm398.8-93.1c-0.1-1.9-0.1-3.4-0.3-4.8q-2.6-18.5-5.4-37-2.8-19.1-5.8-38.2-2.3-14.2-4.7-28.5c-0.9-5.3-1.5-10.6-4.5-15.3-6.6-10.1-16.2-14.1-27.7-14.8-9.5-0.5-18.7 1.6-27.9 3.4-25.3 5-49.9 12.8-73.7 22.7-14.8 6.1-29.3 12.8-43.9 19.2-0.8 0.3-1.5 0.9-2.4 1.5q15.9 7.3 30.7 14.2v9.7c-2.2-0.8-4-1.5-5.6-2.3-15.2-7-30.2-14.1-45.4-21.1-23-10.6-46.1-21.3-69.2-31.8-10.8-4.8-21.8-9.2-32.8-13.4-6.7-2.6-13.6-2.8-20.6-0.6-10.9 3.5-18 10.4-20.3 21.8-1.3 6.8-2.3 13.7-3.4 20.6q-3.1 19-6.1 38.1-2.4 15.2-4.7 30.5c-1.3 8.6-2.6 17.2-3.8 26.1zm-196.6-103.4c-10.5-7.2-78.1-29-85.4-27.8 24.6 11.1 49.6 22.4 74.4 33.6 3.8-2 7.3-3.9 11-5.8z\"/>\n            <path fill-rule=\"evenodd\" class=\"red\" d=\"m485.9 328.3c-8.5-2.1-16.5-3.9-24.6-6-32.5-8-63.9-18.9-93.6-34.4-13.7-7.2-26.9-15.3-37.3-27.2-4.9-5.6-9.3-11.5-11.1-18.9-3.2-12.7-0.7-24.2 7.5-34.5 8.1-10 18.4-17.2 29.6-23.3 17.1-9.3 35.3-15.7 54.1-20.7 20.6-5.6 41.5-9.5 62.6-12.6 4.2-0.6 8.5-0.8 13.3-1.2-0.6 4.4-1.2 8-1.6 11.1-12.1 2-23.8 3.6-35.4 5.9-28.3 5.6-56.3 12.3-82.4 25.3-10.4 5.1-20.5 10.7-28.6 19.3-5.1 5.5-8.6 11.8-9.1 19.3-0.2 3.6 0.7 7.6 2.1 10.9 1.7 4 4.3 7.8 7.1 11.1 9.6 11.3 22.3 18.5 35.3 25.2 19.8 10.1 40.2 18.7 61.6 24.7 18.9 5.2 37.9 10 57 14.2 17 3.7 34.3 6.2 51.5 9.1 8.4 1.4 16.9 2.8 25.4 3.8 8.2 1.1 16.5 1.8 24.7 2.6 12.2 1.1 24.3 2.4 36.5 3.2 12.1 0.7 24.2 1 36.3 1.3 10.2 0.3 20.4 0.5 30.6 0.4 23.3-0.3 46.5-0.8 69.7-2.7 13.1-1.1 26.2-2.3 39.2-3.9 26-3.1 51.8-7 77.5-12.2 29.9-6 59.7-13 88.5-23.5 20.5-7.4 40.5-15.9 58.8-28.1 7.6-5.1 14.7-11 19.9-18.7 8.1-11.8 7.4-23.3-1.8-34.4-5-5.9-11.2-10.4-17.8-14.4-15.4-9.4-31.9-16-49.1-21.1-19.3-5.8-39-10.1-58.8-13.5-7.4-1.2-14.8-2.2-22.8-3.4-0.6-3.2-1.2-6.7-1.9-10.7 2.9 0 5.5-0.3 8 0.1 11.4 1.7 22.8 3.2 34.1 5.5 14.3 2.9 28.6 6 42.6 9.8 21.9 6.1 42.8 14.7 61.3 28.1 9.4 6.8 17.3 15 21.2 26.3 4.1 12 1.9 23.3-5.1 33.7-7.3 10.6-17.1 18.6-27.9 25.3-19.6 12.3-40.8 21.2-62.5 28.9-22.7 8.1-46.1 14-69.7 19.2-1.3 0.3-2.6 0.6-4.3 1-1.3 6.2-2.7 12.5-4 18.7-4.8 23.3-9.6 46.7-14.3 70-5 24.4-9.8 48.8-14.7 73.2-3 14.8-5.9 29.6-9.1 44.4-0.5 2.3-2.1 4.7-3.7 6.5-10.1 11.4-20.3 22.6-30.6 33.8-16.6 18.2-33.1 36.4-49.8 54.5-12.3 13.4-24.7 26.6-37.2 40h-84.4c-10.3-11.1-20.8-22.3-31.2-33.5q-30.9-33.6-61.7-67.2c-7.9-8.6-15.6-17.4-23.7-25.8-3.6-3.8-5.5-8.1-6.4-13.1-4.4-22.7-9-45.3-13.5-67.9q-5-25.6-10.1-51.2-6.2-31.2-12.3-62.3c-1.3-6.6-2.6-13.2-3.9-20zm35.5 123.2c36.1 33.8 72.5 67.9 108.9 101.9 15-5.7 29.7-11.3 44.8-17 3.4 3.5 6.8 7.1 10.4 10.9v-38.9h-20.8v-136.3c-0.7 0.4-1 0.5-1.2 0.7-0.7 0.8-1.4 1.6-2.2 2.4-9.6 10-20.4 18.3-33.6 22.9-15.9 5.7-32 7.3-48.7 3.5-16.4-3.8-31.1-11.1-43.9-21.9-10.3-8.8-20-18.3-30-27.5q-1.5-1.3-2.9-2.6-0.5 0.3-0.9 0.5c6.6 33.6 13.3 67.3 20.1 101.4zm183.3 84.8q23.7 8.8 46.7 17.4c6.1-5.7 11.8-11 17.6-16.3 14.7-13.5 29.4-27.1 44.1-40.7 1-0.9 1.8-2 2.5-3.2 8.6-14.6 17.4-29 25.6-43.8 10.6-18.9 20.6-38.1 30.9-57.1 0.5-1 0.8-2 1-3.1q2.7-13.6 5.3-27.2c0.8-3.9 1.5-7.9 2.2-11.9-2.7 1.5-4.7 3.2-6.7 5-8.8 7.9-17.2 16.3-26.4 23.7-21.5 17.2-45.8 26.7-73.9 24.1-17.9-1.7-33.8-8.5-47.4-20.2-4.2-3.5-7.8-7.6-12.4-12.1v137.8h-19.2v39.2c3.6-4.2 6.9-7.9 10.1-11.6zm36.9 108.1c0.6-0.5 0.8-0.6 1-0.8 24-26.2 48-52.4 71.9-78.6 9.3-10.2 18.4-20.7 27.5-31.1 1.3-1.5 2.4-3.4 2.8-5.3 3.5-17.1 6.9-34.3 10.2-51.5 0.6-2.6 1-5.3 1.5-8.2-43.8 40.5-87.1 80.6-130.3 120.6h-31.9c0.3 18.4-0.2 36.6 0.2 54.9zm-86.1-54.9c-43.3-40.4-86.6-80.7-130.5-121.6 0.2 1.9 0.2 2.9 0.4 3.8 3.2 16.3 6.6 32.5 9.5 48.8 1.4 7.4 4.1 13.6 9.3 19.2 18.2 19.5 36 39.5 54.1 59.2 13.9 15.2 28.1 30.4 41.9 45.3h45.4v-54.7zm57.9-239.6c3.6 7.3 7.6 13.6 12.6 19 20.1 22 45.2 28.8 73.9 23.2 16.3-3.2 30.7-11 43.1-21.7 13.7-11.7 26.6-24.3 39.8-36.6 0.6-0.5 1-1.3 1.4-1.9-28.3 4.1-56.4 8.3-84.5 12.3-28.4 4-56.9 5.4-86.3 5.7zm-47.1 1.1q-0.4-0.6-0.7-1.1c-56.2-0.4-111.5-8.5-166.7-18.4 0.9 1.6 1.9 2.9 3.1 4.1q6.5 6.4 13 12.7c11.4 11 22.8 22.2 36.1 30.9 20.8 13.7 43.2 18.6 67.6 11.8 16.3-4.7 29.5-14 39.6-27.7 2.9-3.9 5.3-8.2 8-12.3zm8.7 145.3h29.9v-145.8h-29.9zm68.2 64.3q-0.2-0.3-0.5-0.6h-48.1v19.1h28.5c6.8-6.2 13.4-12.3 20.1-18.5zm-84.5 18.3h26.9v-18.8h-47.5c7.3 6.7 14 12.8 20.6 18.8zm207.3-153.5l-0.4-0.1c-8.7 15.3-17.3 30.6-26.3 46.6 7.3-6.7 14.1-12.9 20.9-19.2q2.9-13.6 5.8-27.3zm-131.7 227.5h-39.4v6h33.7c1.8-1.9 3.4-3.6 5.7-6zm-86.4-0.1c2.3 2.5 3.9 4.4 5.3 6h32.3v-6z\"/>\n        </g>\n    </g>\n</svg>\n"
  },
  {
    "path": "resources/views/components/authentication-card-logo.blade.php",
    "content": "<a href=\"/\">\n    <svg class=\"w-48 h-48\" version=\"1.2\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1388 893\" {{ $attributes }}>\n        <style>\n            .red { fill: #d14d41 }\n            .white { fill: #E6E4D9 }\n        </style>\n        <g>\n            <g>\n                <path class=\"white\" d=\"m231.9 718.5l-98.6 158.5q-10.3 16.9-18.7 16.9-8.1 0-18.4-16.9l-96.5-158.5h38.6l77.1 128.2 77.8-128.2z\"/>\n                <path class=\"red\" d=\"m281.4 891.1h-30v-172.6h30z\"/>\n                <path class=\"red\" d=\"m514.7 891.1h-109.7q-37.6 0-62.3-25-24.6-25-24.6-63.2 0-37.9 24.4-61.1 24.4-23.3 62.5-23.3h97.6v30.1h-97.6q-24.7 0-40.8 15.9-16 15.9-16 41 0 24.7 16 40.2 16.1 15.4 40.8 15.4h79.6v-37.7h-81.9v-27.5h112z\"/>\n                <path class=\"red\" d=\"m576.9 891.1h-30.1v-172.6h30.1z\"/>\n                <path class=\"red\" d=\"m758.8 891.1h-148.8v-172.6h30v142.6h118.8z\"/>\n                <path class=\"red\" d=\"m1003.2 891.1l-38.6-0.2-29.1-48.3h-81.9l18.2-29.8h45.7l-30-49.8-77.9 127.9h-38.6l93.7-155.9q3.6-6.1 9.9-11.2 7.6-5.8 13.6-5.8 6.6 0 13.7 5.6 6 4.8 9.8 11.4z\"/>\n                <path class=\"red\" d=\"m1190 876.2q0 16.7-11.4 16.7-8.3 0-18.7-10.3l-110.7-111.3v119.8h-29.8v-157.7q0-6 3.3-10.4 3.3-4.5 9.3-4.5 8.4 0 17.2 8.9l110.7 110.9v-119.8h30.1z\"/>\n                <path class=\"red\" d=\"m1386.3 748.6h-67.9v142.5h-29.9v-142.5h-68.2v-30.1h166z\"/>\n                <path fill-rule=\"evenodd\" class=\"red\" d=\"m482.4 243.2c0-1.2-0.2-2.3 0-3.5 1.2-10.1 2.5-20.2 3.7-30.3 1.8-14 3.4-28 5.2-42 2-16.4 3.9-32.8 6.2-49.1 2.6-18.2 5.5-36.3 8.3-54.5 1.6-10.3 3.2-20.7 5-31 3-17.6 18.1-30.6 36-32.2 13.2-1.2 26 1 38.8 3.1 34.5 5.8 67.6 16.2 99.6 30.2 2 0.8 4 1.6 6 2.4 2.2 1 4.5 1.9 6.9 2.8 5-2.1 9.9-4.5 15-6.5 16.3-6.4 32.5-13.1 49-18.8 17-5.8 34.5-10 52.4-12.2 6-0.7 12-1.5 18-1.3 11.9 0.3 23.1 3.2 31.7 12.4 5.7 6.1 9.3 13.1 10.6 21.3 2.5 16.3 5.3 32.5 7.8 48.8q3.5 23 6.7 46.1c2.2 16 4.3 31.9 6.3 47.9 2.3 19.4 4.4 38.9 6.6 58.4 0.3 2.6 0.7 5.2 1 7.9l-0.9 0.1q-1.1-5.6-2.3-11.2c-3.2-1-403.2-1.6-414.5-0.5-0.9 3.8-1.7 7.7-2.6 11.7q-0.3 0-0.5 0zm398.8-93.1c-0.1-1.9-0.1-3.4-0.3-4.8q-2.6-18.5-5.4-37-2.8-19.1-5.8-38.2-2.3-14.2-4.7-28.5c-0.9-5.3-1.5-10.6-4.5-15.3-6.6-10.1-16.2-14.1-27.7-14.8-9.5-0.5-18.7 1.6-27.9 3.4-25.3 5-49.9 12.8-73.7 22.7-14.8 6.1-29.3 12.8-43.9 19.2-0.8 0.3-1.5 0.9-2.4 1.5q15.9 7.3 30.7 14.2v9.7c-2.2-0.8-4-1.5-5.6-2.3-15.2-7-30.2-14.1-45.4-21.1-23-10.6-46.1-21.3-69.2-31.8-10.8-4.8-21.8-9.2-32.8-13.4-6.7-2.6-13.6-2.8-20.6-0.6-10.9 3.5-18 10.4-20.3 21.8-1.3 6.8-2.3 13.7-3.4 20.6q-3.1 19-6.1 38.1-2.4 15.2-4.7 30.5c-1.3 8.6-2.6 17.2-3.8 26.1zm-196.6-103.4c-10.5-7.2-78.1-29-85.4-27.8 24.6 11.1 49.6 22.4 74.4 33.6 3.8-2 7.3-3.9 11-5.8z\"/>\n                <path fill-rule=\"evenodd\" class=\"red\" d=\"m485.9 328.3c-8.5-2.1-16.5-3.9-24.6-6-32.5-8-63.9-18.9-93.6-34.4-13.7-7.2-26.9-15.3-37.3-27.2-4.9-5.6-9.3-11.5-11.1-18.9-3.2-12.7-0.7-24.2 7.5-34.5 8.1-10 18.4-17.2 29.6-23.3 17.1-9.3 35.3-15.7 54.1-20.7 20.6-5.6 41.5-9.5 62.6-12.6 4.2-0.6 8.5-0.8 13.3-1.2-0.6 4.4-1.2 8-1.6 11.1-12.1 2-23.8 3.6-35.4 5.9-28.3 5.6-56.3 12.3-82.4 25.3-10.4 5.1-20.5 10.7-28.6 19.3-5.1 5.5-8.6 11.8-9.1 19.3-0.2 3.6 0.7 7.6 2.1 10.9 1.7 4 4.3 7.8 7.1 11.1 9.6 11.3 22.3 18.5 35.3 25.2 19.8 10.1 40.2 18.7 61.6 24.7 18.9 5.2 37.9 10 57 14.2 17 3.7 34.3 6.2 51.5 9.1 8.4 1.4 16.9 2.8 25.4 3.8 8.2 1.1 16.5 1.8 24.7 2.6 12.2 1.1 24.3 2.4 36.5 3.2 12.1 0.7 24.2 1 36.3 1.3 10.2 0.3 20.4 0.5 30.6 0.4 23.3-0.3 46.5-0.8 69.7-2.7 13.1-1.1 26.2-2.3 39.2-3.9 26-3.1 51.8-7 77.5-12.2 29.9-6 59.7-13 88.5-23.5 20.5-7.4 40.5-15.9 58.8-28.1 7.6-5.1 14.7-11 19.9-18.7 8.1-11.8 7.4-23.3-1.8-34.4-5-5.9-11.2-10.4-17.8-14.4-15.4-9.4-31.9-16-49.1-21.1-19.3-5.8-39-10.1-58.8-13.5-7.4-1.2-14.8-2.2-22.8-3.4-0.6-3.2-1.2-6.7-1.9-10.7 2.9 0 5.5-0.3 8 0.1 11.4 1.7 22.8 3.2 34.1 5.5 14.3 2.9 28.6 6 42.6 9.8 21.9 6.1 42.8 14.7 61.3 28.1 9.4 6.8 17.3 15 21.2 26.3 4.1 12 1.9 23.3-5.1 33.7-7.3 10.6-17.1 18.6-27.9 25.3-19.6 12.3-40.8 21.2-62.5 28.9-22.7 8.1-46.1 14-69.7 19.2-1.3 0.3-2.6 0.6-4.3 1-1.3 6.2-2.7 12.5-4 18.7-4.8 23.3-9.6 46.7-14.3 70-5 24.4-9.8 48.8-14.7 73.2-3 14.8-5.9 29.6-9.1 44.4-0.5 2.3-2.1 4.7-3.7 6.5-10.1 11.4-20.3 22.6-30.6 33.8-16.6 18.2-33.1 36.4-49.8 54.5-12.3 13.4-24.7 26.6-37.2 40h-84.4c-10.3-11.1-20.8-22.3-31.2-33.5q-30.9-33.6-61.7-67.2c-7.9-8.6-15.6-17.4-23.7-25.8-3.6-3.8-5.5-8.1-6.4-13.1-4.4-22.7-9-45.3-13.5-67.9q-5-25.6-10.1-51.2-6.2-31.2-12.3-62.3c-1.3-6.6-2.6-13.2-3.9-20zm35.5 123.2c36.1 33.8 72.5 67.9 108.9 101.9 15-5.7 29.7-11.3 44.8-17 3.4 3.5 6.8 7.1 10.4 10.9v-38.9h-20.8v-136.3c-0.7 0.4-1 0.5-1.2 0.7-0.7 0.8-1.4 1.6-2.2 2.4-9.6 10-20.4 18.3-33.6 22.9-15.9 5.7-32 7.3-48.7 3.5-16.4-3.8-31.1-11.1-43.9-21.9-10.3-8.8-20-18.3-30-27.5q-1.5-1.3-2.9-2.6-0.5 0.3-0.9 0.5c6.6 33.6 13.3 67.3 20.1 101.4zm183.3 84.8q23.7 8.8 46.7 17.4c6.1-5.7 11.8-11 17.6-16.3 14.7-13.5 29.4-27.1 44.1-40.7 1-0.9 1.8-2 2.5-3.2 8.6-14.6 17.4-29 25.6-43.8 10.6-18.9 20.6-38.1 30.9-57.1 0.5-1 0.8-2 1-3.1q2.7-13.6 5.3-27.2c0.8-3.9 1.5-7.9 2.2-11.9-2.7 1.5-4.7 3.2-6.7 5-8.8 7.9-17.2 16.3-26.4 23.7-21.5 17.2-45.8 26.7-73.9 24.1-17.9-1.7-33.8-8.5-47.4-20.2-4.2-3.5-7.8-7.6-12.4-12.1v137.8h-19.2v39.2c3.6-4.2 6.9-7.9 10.1-11.6zm36.9 108.1c0.6-0.5 0.8-0.6 1-0.8 24-26.2 48-52.4 71.9-78.6 9.3-10.2 18.4-20.7 27.5-31.1 1.3-1.5 2.4-3.4 2.8-5.3 3.5-17.1 6.9-34.3 10.2-51.5 0.6-2.6 1-5.3 1.5-8.2-43.8 40.5-87.1 80.6-130.3 120.6h-31.9c0.3 18.4-0.2 36.6 0.2 54.9zm-86.1-54.9c-43.3-40.4-86.6-80.7-130.5-121.6 0.2 1.9 0.2 2.9 0.4 3.8 3.2 16.3 6.6 32.5 9.5 48.8 1.4 7.4 4.1 13.6 9.3 19.2 18.2 19.5 36 39.5 54.1 59.2 13.9 15.2 28.1 30.4 41.9 45.3h45.4v-54.7zm57.9-239.6c3.6 7.3 7.6 13.6 12.6 19 20.1 22 45.2 28.8 73.9 23.2 16.3-3.2 30.7-11 43.1-21.7 13.7-11.7 26.6-24.3 39.8-36.6 0.6-0.5 1-1.3 1.4-1.9-28.3 4.1-56.4 8.3-84.5 12.3-28.4 4-56.9 5.4-86.3 5.7zm-47.1 1.1q-0.4-0.6-0.7-1.1c-56.2-0.4-111.5-8.5-166.7-18.4 0.9 1.6 1.9 2.9 3.1 4.1q6.5 6.4 13 12.7c11.4 11 22.8 22.2 36.1 30.9 20.8 13.7 43.2 18.6 67.6 11.8 16.3-4.7 29.5-14 39.6-27.7 2.9-3.9 5.3-8.2 8-12.3zm8.7 145.3h29.9v-145.8h-29.9zm68.2 64.3q-0.2-0.3-0.5-0.6h-48.1v19.1h28.5c6.8-6.2 13.4-12.3 20.1-18.5zm-84.5 18.3h26.9v-18.8h-47.5c7.3 6.7 14 12.8 20.6 18.8zm207.3-153.5l-0.4-0.1c-8.7 15.3-17.3 30.6-26.3 46.6 7.3-6.7 14.1-12.9 20.9-19.2q2.9-13.6 5.8-27.3zm-131.7 227.5h-39.4v6h33.7c1.8-1.9 3.4-3.6 5.7-6zm-86.4-0.1c2.3 2.5 3.9 4.4 5.3 6h32.3v-6z\"/>\n            </g>\n        </g>\n    </svg>\n</a>\n"
  },
  {
    "path": "resources/views/components/authentication-card.blade.php",
    "content": "<div class=\"min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-base-black relative\">\n    <!-- Noise overlay -->\n    <div class=\"absolute inset-0 opacity-[0.04]\" style=\"background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIj48ZmVUdXJidWxlbmNlIGJhc2VGcmVxdWVuY3k9Ii43NSIgc3RpdGNoVGlsZXM9InN0aXRjaCIgdHlwZT0iZnJhY3RhbE5vaXNlIi8+PGZlQ29sb3JNYXRyaXggdHlwZT0ic2F0dXJhdGUiIHZhbHVlcz0iMCIvPjwvZmlsdGVyPjxwYXRoIGQ9Ik0wIDBoMzAwdjMwMEgweiIgZmlsdGVyPSJ1cmwoI2EpIiBvcGFjaXR5PSIuMDUiLz48L3N2Zz4=');\"></div>\n    \n    <!-- Decorative gradient orbs -->\n    <div class=\"absolute top-20 left-10 w-96 h-96 bg-gradient-to-br from-red/20 to-orange/10 rounded-full blur-3xl opacity-30 animate-pulse-glow\"></div>\n    <div class=\"absolute bottom-20 right-10 w-80 h-80 bg-gradient-to-br from-blue/20 to-indigo/10 rounded-full blur-3xl opacity-20 animate-pulse-glow\" style=\"animation-delay: -2s;\"></div>\n    \n    <div class=\"relative z-10\">\n        {{ $logo }}\n    </div>\n\n    <div class=\"w-full sm:max-w-md mt-6 px-8 py-8 bg-base-900 border border-base-700 shadow-xl overflow-hidden sm:rounded-lg relative z-10\">\n        {{ $slot }}\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/banner.blade.php",
    "content": "@props(['style' => session('flash.bannerStyle', 'success'), 'message' => session('flash.banner')])\n\n<div x-data=\"{{ json_encode(['show' => true, 'style' => $style, 'message' => $message]) }}\"\n            :class=\"{ \n                'bg-gradient-to-r from-green to-green-light border-green-light/30': style == 'success', \n                'bg-gradient-to-r from-red to-red-light border-red-light/30': style == 'danger', \n                'bg-gradient-to-r from-blue to-blue-light border-blue-light/30': style != 'success' && style != 'danger' \n            }\"\n            class=\"border shadow-lg\"\n            style=\"display: none;\"\n            x-show=\"show && message\"\n            x-transition:enter=\"transition ease-out duration-300\"\n            x-transition:enter-start=\"opacity-0 -translate-y-2\"\n            x-transition:enter-end=\"opacity-100 translate-y-0\"\n            x-transition:leave=\"transition ease-in duration-200\"\n            x-transition:leave-start=\"opacity-100\"\n            x-transition:leave-end=\"opacity-0\"\n            x-on:banner-message.window=\"\n                style = event.detail.style;\n                message = event.detail.message;\n                show = true;\n            \">\n    <div class=\"max-w-7xl mx-auto py-3 px-4 sm:px-6 lg:px-8\">\n        <div class=\"flex items-center justify-between flex-wrap\">\n            <div class=\"w-0 flex-1 flex items-center min-w-0\">\n                <span class=\"flex p-2 rounded-lg bg-white/20 backdrop-blur-sm\">\n                    <svg x-show=\"style == 'success'\" class=\"h-5 w-5 text-white\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n                    </svg>\n                    <svg x-show=\"style == 'danger'\" class=\"h-5 w-5 text-white\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z\" />\n                    </svg>\n                    <svg x-show=\"style != 'success' && style != 'danger'\" class=\"h-5 w-5 text-white\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z\" />\n                    </svg>\n                </span>\n\n                <p class=\"ms-3 font-semibold text-sm text-white\" x-text=\"message\"></p>\n            </div>\n\n            <div class=\"shrink-0 sm:ms-3\">\n                <button\n                    type=\"button\"\n                    class=\"-me-1 flex p-2 rounded-lg hover:bg-white/20 focus:bg-white/20 focus:outline-hidden transition-colors duration-200\"\n                    aria-label=\"Dismiss\"\n                    x-on:click=\"show = false\">\n                    <svg class=\"h-5 w-5 text-white\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n                    </svg>\n                </button>\n            </div>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/button.blade.php",
    "content": "<button {{ $attributes->merge(['type' => 'submit', 'class' => 'group inline-flex items-center justify-center gap-2 px-6 py-3 border-2 border-base-700 hover:border-indigo rounded-xl font-semibold text-sm text-base-100 shadow-lg hover:shadow-xl hover:bg-base-800/50 backdrop-blur-sm focus:outline-hidden focus:ring-2 focus:ring-indigo focus:ring-offset-2 focus:ring-offset-base-black disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-base-700 disabled:hover:bg-transparent transition-all duration-300']) }}>\n    {{ $slot }}\n</button>\n"
  },
  {
    "path": "resources/views/components/card.blade.php",
    "content": "@props(['padding' => true])\n\n<div {{ $attributes->merge(['class' => 'border border-base-700 shadow-xl rounded-xl overflow-hidden backdrop-blur-sm relative ' . ($padding ? 'px-6 py-8 sm:p-8' : '')]) }}>\n    <!-- Multi-stop gradient background to prevent banding -->\n    <div class=\"absolute inset-0 -z-10\" \n         style=\"background: \n                linear-gradient(135deg, \n                    rgba(35, 35, 51, 1) 0%, \n                    rgba(33, 33, 48, 1) 10%,\n                    rgba(31, 31, 45, 1) 20%,\n                    rgba(29, 29, 42, 1) 30%,\n                    rgba(28, 28, 40, 1) 40%,\n                    rgba(27, 27, 38, 1) 50%,\n                    rgba(26, 26, 36, 1) 60%,\n                    rgba(26, 26, 36, 1) 70%,\n                    rgba(26, 26, 36, 1) 80%,\n                    rgba(26, 26, 36, 1) 90%,\n                    rgba(26, 26, 36, 1) 100%\n                ),\n                url('data:image/svg+xml,%3Csvg viewBox=%220 0 400 400%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noiseFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%221.5%22 numOctaves=%225%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noiseFilter)%22 opacity=%220.12%22/%3E%3C/svg%3E');\">\n    </div>\n    \n    <!-- Subtle gradient overlay for depth with noise -->\n    <div class=\"absolute inset-0 pointer-events-none\"\n         style=\"background: \n                linear-gradient(180deg, \n                    rgba(45, 45, 66, 0.1) 0%, \n                    rgba(45, 45, 66, 0.075) 10%,\n                    rgba(45, 45, 66, 0.05) 20%,\n                    rgba(45, 45, 66, 0.025) 30%,\n                    rgba(45, 45, 66, 0.01) 40%,\n                    transparent 50%\n                ),\n                url('data:image/svg+xml,%3Csvg viewBox=%220 0 300 300%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22grainFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%221%22 numOctaves=%224%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23grainFilter)%22 opacity=%220.08%22/%3E%3C/svg%3E');\"></div>\n    \n    <div class=\"relative\">\n        {{ $slot }}\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/checkbox.blade.php",
    "content": "<input type=\"checkbox\" {!! $attributes->merge(['class' => 'rounded-sm bg-base-800 border-base-700 text-red focus:ring-red focus:ring-offset-base-900']) !!}>\n"
  },
  {
    "path": "resources/views/components/confirmation-modal.blade.php",
    "content": "@props(['id' => null, 'maxWidth' => null])\n\n<x-modal :id=\"$id\" :maxWidth=\"$maxWidth\" {{ $attributes }}>\n    <div class=\"bg-base-900 px-4 pt-5 pb-4 sm:p-6 sm:pb-4\">\n        <div class=\"sm:flex sm:items-start\">\n            <div class=\"mx-auto shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red/20 sm:mx-0 sm:h-10 sm:w-10\">\n                <svg class=\"h-6 w-6 text-red\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\" />\n                </svg>\n            </div>\n\n            <div class=\"mt-3 text-center sm:mt-0 sm:ms-4 sm:text-start\">\n                <h3 class=\"text-lg font-medium text-base-100\">\n                    {{ $title }}\n                </h3>\n\n                <div class=\"mt-4 text-sm text-base-300\">\n                    {{ $content }}\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <div class=\"flex flex-row justify-end px-6 py-4 bg-base-850 text-end\">\n        {{ $footer }}\n    </div>\n</x-modal>\n"
  },
  {
    "path": "resources/views/components/confirms-password.blade.php",
    "content": "@props(['title' => __('Confirm Password'), 'content' => __('For your security, please confirm your password to continue.'), 'button' => __('Confirm')])\n\n@php\n    $confirmableId = md5($attributes->wire('then'));\n@endphp\n\n<span\n    {{ $attributes->wire('then') }}\n    x-data\n    x-ref=\"span\"\n    x-on:click=\"$wire.startConfirmingPassword('{{ $confirmableId }}')\"\n    x-on:password-confirmed.window=\"setTimeout(() => $event.detail.id === '{{ $confirmableId }}' && $refs.span.dispatchEvent(new CustomEvent('then', { bubbles: false })), 250);\"\n>\n    {{ $slot }}\n</span>\n\n@once\n<x-dialog-modal wire:model.live=\"confirmingPassword\">\n    <x-slot name=\"title\">\n        {{ $title }}\n    </x-slot>\n\n    <x-slot name=\"content\">\n        {{ $content }}\n\n        <div class=\"mt-4\" x-data=\"{}\" x-on:confirming-password.window=\"setTimeout(() => $refs.confirmable_password.focus(), 250)\">\n            <x-input type=\"password\" class=\"mt-1 block w-3/4\" placeholder=\"{{ __('Password') }}\" autocomplete=\"current-password\"\n                        x-ref=\"confirmable_password\"\n                        wire:model=\"confirmablePassword\"\n                        wire:keydown.enter=\"confirmPassword\" />\n\n            <x-input-error for=\"confirmable_password\" class=\"mt-2\" />\n        </div>\n    </x-slot>\n\n    <x-slot name=\"footer\">\n        <x-form.button class=\"bg-base-800\" wire:click=\"stopConfirmingPassword\" wire:loading.attr=\"disabled\">\n            {{ __('Cancel') }}\n        </x-form.button>\n\n        <x-form.button class=\"ms-3 bg-red\" dusk=\"confirm-password-button\" wire:click=\"confirmPassword\" wire:loading.attr=\"disabled\">\n            {{ $button }}\n        </x-form.button>\n    </x-slot>\n</x-dialog-modal>\n@endonce\n"
  },
  {
    "path": "resources/views/components/create-button-dropdown.blade.php",
    "content": "@props(['model'])\n@can('create', $model)\n    <li {{ $attributes->merge(['class' => 'cursor-pointer text-base-100 text-sm font-medium p-3 transition-all hover:bg-gradient-to-r hover:from-red/20 hover:to-orange/20 rounded-lg']) }}\n        wire:navigate.hover>\n        <div class=\"flex items-center gap-2\">\n            <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4v16m8-8H4\"></path>\n            </svg>\n            <span>{{ $slot }}</span>\n        </div>\n    </li>\n@else\n    <li {{ $attributes->merge(['class' => 'cursor-not-allowed text-base-400 text-sm p-3 transition-all opacity-60'])->except(['href']) }}\n        x-on:click=\"open = false; alert('@lang('Your current plan does not allow to create this resource')')\">\n        <div class=\"flex items-center gap-2\">\n            @svg('tni-x-circle-o', 'size-5 text-red')\n            <span>{{ $slot }}</span>\n        </div>\n    </li>\n@endcan\n"
  },
  {
    "path": "resources/views/components/create-button.blade.php",
    "content": "@props(['model'])\n@can('create', $model)\n    <x-form.button {{ $attributes->merge(['class' => 'bg-gradient-to-r from-red via-orange to-red bg-[length:200%] bg-left hover:bg-right transition-[background-position] duration-300 border-transparent']) }} wire:navigate.hover>\n        <span class=\"flex items-center gap-2\">\n            {{ $slot }}\n            <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4v16m8-8H4\"></path>\n            </svg>\n        </span>\n    </x-form.button>\n@else\n    <x-form.button\n        {{ $attributes->merge(['class' => 'bg-base-700/50 hover:bg-base-600/50 cursor-not-allowed has-tooltip opacity-60 border-base-600/50'])->except(['href']) }}\n        disabled>\n        <span class=\"tooltip rounded-lg shadow-lg bg-base-900 border border-base-700 text-base-100 mt-8 px-3 py-2 text-xs\">\n            @lang('Your current plan does not allow to create this resource')\n        </span>\n        <span>\n            {{ $slot }}\n        </span>\n    </x-form.button>\n@endcan\n"
  },
  {
    "path": "resources/views/components/danger-button.blade.php",
    "content": "<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center justify-center gap-2 px-6 py-2.5 bg-gradient-to-r from-red to-red-light border border-transparent rounded-lg font-semibold text-sm text-base-100 shadow-lg shadow-red/20 hover:from-red-light hover:to-red hover:shadow-xl hover:shadow-red/30 active:from-red-dark active:to-red focus:outline-hidden focus:ring-2 focus:ring-red-light focus:ring-offset-2 dark:focus:ring-offset-base-black transition-all duration-200']) }}>\n    {{ $slot }}\n</button>\n"
  },
  {
    "path": "resources/views/components/dialog-modal.blade.php",
    "content": "@props(['id' => null, 'maxWidth' => null])\n\n<x-modal :id=\"$id\" :maxWidth=\"$maxWidth\" {{ $attributes }}>\n    <div class=\"px-6 py-4\">\n        <div class=\"text-lg font-medium text-white\">\n            {{ $title }}\n        </div>\n\n        <div class=\"mt-4 text-sm text-gray-50\">\n            {{ $content }}\n        </div>\n    </div>\n\n    <div class=\"flex flex-row justify-end px-6 py-4 bg-base-950 text-end\">\n        {{ $footer }}\n    </div>\n</x-modal>\n"
  },
  {
    "path": "resources/views/components/dropdown-link.blade.php",
    "content": "<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-base-100 hover:bg-red focus:outline-hidden focus:bg-base-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>\n"
  },
  {
    "path": "resources/views/components/dropdown.blade.php",
    "content": "@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-black', 'dropdownClasses' => ''])\n\n@php\nswitch ($align) {\n    case 'left':\n        $alignmentClasses = 'ltr:origin-top-left rtl:origin-top-right start-0';\n        break;\n    case 'top':\n        $alignmentClasses = 'origin-top';\n        break;\n    case 'none':\n    case 'false':\n        $alignmentClasses = '';\n        break;\n    case 'right':\n    default:\n        $alignmentClasses = 'ltr:origin-top-right rtl:origin-top-left end-0';\n        break;\n}\n\nswitch ($width) {\n    case '48':\n        $width = 'w-48';\n        break;\n}\n@endphp\n\n<div class=\"relative z-50\" x-data=\"{ open: false }\" @click.away=\"open = false\" @close.stop=\"open = false\">\n    <div @click=\"open = ! open\">\n        {{ $trigger }}\n    </div>\n\n    <div x-show=\"open\"\n            x-transition:enter=\"transition ease-out duration-200\"\n            x-transition:enter-start=\"transform opacity-0 scale-95\"\n            x-transition:enter-end=\"transform opacity-100 scale-100\"\n            x-transition:leave=\"transition ease-in duration-75\"\n            x-transition:leave-start=\"transform opacity-100 scale-100\"\n            x-transition:leave-end=\"transform opacity-0 scale-95\"\n            class=\"absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }} {{ $dropdownClasses }}\"\n            style=\"display: none;\"\n            @click=\"open = false\">\n        <div class=\"rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}\">\n            {{ $content }}\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/form/button.blade.php",
    "content": "@php\n$tag = isset($href) ? 'a' : 'button';\n\n// Define color variants based on classes\n$hasBlue = str_contains($attributes->get('class', ''), 'bg-blue');\n$hasRed = str_contains($attributes->get('class', ''), 'bg-red');\n$hasGradient = str_contains($attributes->get('class', ''), 'gradient');\n\n// Default classes for neutral buttons (no color specified)\n$defaultClasses = 'bg-base-900/50 border-base-800/50 hover:bg-base-800/50 hover:border-base-700';\n\n// Color-specific classes\n$blueClasses = 'bg-base-900/50 border-blue/50 text-blue hover:bg-blue/10 hover:border-blue hover:text-blue-light';\n$redClasses = 'bg-red/10 border-red/50 text-red hover:bg-red/20 hover:border-red hover:text-red-light';\n$gradientClasses = 'border-transparent';\n\n// Determine which classes to use\n$colorClasses = $defaultClasses;\nif ($hasBlue) {\n    $colorClasses = $blueClasses;\n} elseif ($hasRed) {\n    $colorClasses = $redClasses;\n} elseif ($hasGradient) {\n    $colorClasses = $gradientClasses;\n}\n@endphp\n\n<{{ $tag }}\n    {{ $attributes->merge(['class' => \"inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-semibold text-base-100 border transition-all duration-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red cursor-pointer {$colorClasses}\"]) }}>\n    {{ $slot }}\n    </{{ $tag }}>\n"
  },
  {
    "path": "resources/views/components/form/checkbox.blade.php",
    "content": "@props(['field', 'name', 'placeholder', 'description' => ''])\n<div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n    <div class=\"flex flex-col justify-center\">\n        <label for=\"{{ $field }}\" class=\"block text-base font-semibold leading-6 text-base-50\">@lang($name)</label>\n        @if($description)\n            <span class=\"text-base-400 text-sm mt-1\">{{ $description }}</span>\n        @endif\n    </div>\n    <div class=\"flex flex-col justify-center\">\n        <div class=\"flex items-center h-10\">\n            <!-- Toggle Switch -->\n            <button type=\"button\"\n                    role=\"switch\"\n                    x-data=\"{ enabled: $wire.entangle('{{ $field }}').live }\"\n                    x-on:click=\"enabled = !enabled\"\n                    :aria-checked=\"enabled.toString()\"\n                    :class=\"enabled ? 'bg-gradient-to-r from-red to-orange' : 'bg-base-700'\"\n                    class=\"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-red focus:ring-offset-2 focus:ring-offset-base-900\">\n                <span class=\"sr-only\">{{ $name }}</span>\n                <span :class=\"enabled ? 'translate-x-5' : 'translate-x-0'\"\n                      class=\"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-base-50 shadow-lg ring-0 transition duration-200 ease-in-out\"></span>\n            </button>\n        </div>\n        @error($field) <span class=\"text-red text-sm mt-1\">{{ $message }}</span> @enderror\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/form/dropdown-button.blade.php",
    "content": "@php\n$tag = $attributes->has('href') ? 'a' : 'button';\n$typeAttr = $tag === 'button' ? 'type=\"button\"' : '';\n@endphp\n\n<{{ $tag }} {{ $attributes->merge(['class' => 'group flex items-center gap-3 w-full px-4 py-2.5 text-sm font-medium text-base-300 hover:text-base-100 hover:bg-base-900/50 transition-all duration-200 border-b border-base-800/30 last:border-b-0']) }} {!! $typeAttr !!}>\n    {{ $slot }}\n</{{ $tag }}>\n"
  },
  {
    "path": "resources/views/components/form/header.blade.php",
    "content": "<div {{ $attributes->merge(['class' => 'mb-8 pb-6 border-b border-base-700/50 relative']) }}>\n    <!-- Subtle background glow with noise to prevent banding -->\n    <div class=\"absolute -inset-x-4 -inset-y-2 blur-xl -z-10\"\n         style=\"background: \n                linear-gradient(90deg, \n                    rgba(239, 68, 68, 0.03) 0%, \n                    rgba(239, 68, 68, 0.025) 10%,\n                    rgba(239, 68, 68, 0.02) 20%,\n                    rgba(239, 68, 68, 0.015) 30%,\n                    rgba(239, 68, 68, 0.01) 40%,\n                    rgba(239, 68, 68, 0.005) 45%,\n                    transparent 50%,\n                    rgba(249, 115, 22, 0.005) 55%,\n                    rgba(249, 115, 22, 0.01) 60%,\n                    rgba(249, 115, 22, 0.015) 70%,\n                    rgba(249, 115, 22, 0.02) 80%,\n                    rgba(249, 115, 22, 0.025) 90%,\n                    rgba(249, 115, 22, 0.03) 100%\n                ),\n                url('data:image/svg+xml,%3Csvg viewBox=%220 0 256 256%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noiseFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%221.2%22 numOctaves=%225%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noiseFilter)%22 opacity=%220.15%22/%3E%3C/svg%3E');\"></div>\n    \n    <h3 class=\"text-xl sm:text-2xl font-bold leading-tight bg-gradient-to-r from-base-50 via-base-100 to-base-200 bg-clip-text text-transparent\">\n        {{ $slot }}\n    </h3>\n    <div class=\"h-1 w-16 rounded-full mt-2 relative overflow-hidden\">\n        <div class=\"absolute inset-0 bg-gradient-to-r from-red via-orange to-transparent\"></div>\n        <div class=\"absolute inset-0 opacity-30 mix-blend-soft-light\" \n             style=\"background-image: url('data:image/svg+xml,%3Csvg viewBox=%220 0 256 256%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noise%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%221%22 numOctaves=%224%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noise)%22/%3E%3C/svg%3E');\"></div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/form/number.blade.php",
    "content": "@props(['field', 'name', 'placeholder' => null, 'description' => ''])\n<div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n    <div class=\"flex flex-col justify-center\">\n        <label for=\"{{ $field }}\" class=\"block text-base font-semibold leading-6 text-base-50\">@lang($name)</label>\n        @if($description)\n            <span class=\"text-base-400 text-sm mt-1\">{{ $description }}</span>\n        @endif\n    </div>\n    <div class=\"flex flex-col justify-center\">\n        <div\n            class=\"flex rounded-lg bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red transition-all duration-200\">\n            <input type=\"number\"\n                   name=\"{{ $name }}\"\n                   id=\"{{ $field }}\"\n                   wire:model.live=\"{{ $field }}\"\n                   placeholder=\"{{ $placeholder ?? '' }}\"\n                   {{ $attributes->class(['flex-1 border-0 bg-transparent py-2.5 px-3 text-base-100 focus:ring-0 sm:text-sm sm:leading-6 placeholder:text-base-500']) }}>\n        </div>\n\n        @error($field) <span class=\"text-red text-sm mt-1\">{{ $message }}</span> @enderror\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/form/password.blade.php",
    "content": "@props(['field', 'name', 'placeholder', 'description' => ''])\n<div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n    <div class=\"flex flex-col justify-center\">\n        <label for=\"{{ $field }}\" class=\"block text-base font-semibold leading-6 text-base-50\">@lang($name)</label>\n        @if($description)\n            <span class=\"text-base-400 text-sm mt-1\">{{ $description }}</span>\n        @endif\n    </div>\n    <div class=\"flex flex-col justify-center\">\n        <div\n            class=\"flex rounded-lg bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red transition-all duration-200\">\n            <input type=\"password\"\n                   name=\"{{ $name }}\"\n                   id=\"{{ $field }}\"\n                   wire:model.live=\"{{ $field }}\"\n                   class=\"flex-1 border-0 bg-transparent py-2.5 px-3 text-base-100 focus:ring-0 sm:text-sm sm:leading-6 placeholder:text-base-500\"\n                   placeholder=\"{{ $placeholder ?? '' }}\">\n        </div>\n\n        @error($field) <span class=\"text-red text-sm mt-1\">{{ $message }}</span> @enderror\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/form/select.blade.php",
    "content": "@props(['field', 'name' => '', 'placeholder', 'description' => '', 'inline' => false])\n\n@if(!$inline && !blank($name))\n    <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n        <div class=\"flex flex-col justify-center\">\n            <label for=\"{{ $field }}\" class=\"block text-base font-semibold leading-6 text-base-50\">@lang($name)</label>\n            @if($description)\n                <span class=\"text-base-400 text-sm mt-1\">{{ $description }}</span>\n            @endif\n        </div>\n        <div class=\"flex flex-col justify-center\">\n@endif\n            <select id=\"{{ $field }}\"\n                    name=\"{{ $field }}\"\n                    wire:model.live=\"{{ $field }}\"\n                    {{ $attributes }}\n                    class=\"block w-full rounded-lg border-0 py-2.5 px-3 text-base-100 bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red disabled:bg-base-950 transition-all duration-200\">\n                {{ $slot }}\n            </select>\n            @error($field) <span class=\"text-red text-sm mt-1\">{{ $message }}</span> @enderror\n@if(!$inline && !blank($name))\n\n        </div>\n\n    </div>\n\n@endif\n"
  },
  {
    "path": "resources/views/components/form/submit-button.blade.php",
    "content": "<div class=\"flex justify-end gap-3 pt-6 border-t border-base-700\">\n    {{ $slot }}\n    <button type=\"submit\"\n        {{ $attributes->merge(['class' => 'inline-flex items-center gap-2 rounded-lg bg-gradient-to-r from-red to-red-light hover:from-red-light hover:to-red px-6 py-2.5 text-sm font-semibold text-base-100 shadow-lg shadow-red/20 hover:shadow-xl hover:shadow-red/30 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-light disabled:opacity-50 transition-all duration-200']) }}>\n        {{ __($submitText ?? 'Create') }}\n    </button>\n</div>\n"
  },
  {
    "path": "resources/views/components/form/text-list.blade.php",
    "content": "@props(['field', 'items' => [], 'name' => '', 'placeholder', 'description' => '', 'live' => true])\n<div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\" \n     x-data=\"{ \n         items: @js($items),\n         addItem() {\n             this.items.push('');\n         },\n         removeItem(index) {\n             this.items.splice(index, 1);\n         }\n     }\"\n     x-init=\"\n         // Listen for Livewire updates\n         Livewire.hook('commit', ({ component, commit, respond }) => {\n             respond(() => {\n                 // After Livewire updates, restore Alpine state from backend\n                 let newItems = @this.get('{{ $field }}') || [];\n                 if (JSON.stringify(this.items) !== JSON.stringify(newItems)) {\n                     this.items = newItems;\n                 }\n             });\n         });\n     \">\n    @if(!blank($name))\n        <div class=\"flex flex-col justify-center\">\n            <label class=\"block text-base font-semibold leading-6 text-base-50\">@lang($name)</label>\n            @if($description)\n                <span class=\"text-base-400 text-sm mt-1\">{{ $description }}</span>\n            @endif\n        </div>\n    @endif\n    <div class=\"flex flex-col justify-center\">\n        <div class=\"space-y-3 mb-4\">\n            <template x-for=\"(item, index) in items\" :key=\"index\">\n                <div class=\"flex gap-2 items-start\">\n                    <div class=\"flex-1 rounded-lg bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red transition-all duration-200\">\n                        <input type=\"text\"\n                               :name=\"'{{ $field }}[' + index + ']'\"\n                               x-model=\"items[index]\"\n                               {{ $attributes->merge(['class' => 'w-full border-0 bg-transparent py-2.5 px-3 text-base-100 focus:ring-0 sm:text-sm sm:leading-6 placeholder:text-base-500']) }}\n                               placeholder=\"{{ $placeholder ?? '' }}\">\n                    </div>\n                    <button \n                        type=\"button\"\n                        @click=\"removeItem(index)\"\n                        x-show=\"items.length > 1\"\n                        class=\"flex items-center justify-center w-10 h-10 rounded-lg bg-red/10 border border-red/30 text-red-light hover:bg-red/20 hover:border-red transition-all duration-200\">\n                        <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\" />\n                        </svg>\n                    </button>\n                </div>\n            </template>\n        </div>\n\n        <button \n            @click=\"addItem()\" \n            type=\"button\"\n            class=\"bg-gradient-to-r from-blue to-blue-light text-white px-4 py-2.5 rounded-lg font-medium hover:shadow-lg hover:shadow-blue/30 transition-all duration-200 inline-flex items-center gap-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue\">\n            <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4v16m8-8H4\" />\n            </svg>\n            @lang('Add')\n        </button>\n\n        <div>\n            @error($field) <span class=\"text-red text-sm mt-1\">@lang($message)</span> @enderror\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/form/text.blade.php",
    "content": "@props(['field', 'name' => '', 'placeholder', 'description' => '', 'live' => true])\n<div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n    @if(!blank($name))\n    <div class=\"flex flex-col justify-center\">\n        <label for=\"{{ $field }}\" class=\"block text-base font-semibold leading-6 text-base-50\">@lang($name)</label>\n        @if($description)\n            <span class=\"text-base-400 text-sm mt-1\">{{ $description }}</span>\n        @endif\n    </div>\n    @endif\n    <div class=\"flex flex-col justify-center\">\n        <div\n            class=\"flex rounded-lg bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red transition-all duration-200\">\n            <input type=\"text\"\n                   name=\"{{ $name }}\"\n                   id=\"{{ $field }}\"\n                   @if($live) wire:model.blur=\"{{ $field }}\" @else wire:model=\"{{ $field }}\" @endif\n                   wire:loading.attr=\"disabled\"\n                   {{ $attributes->merge(['class' => 'flex-1 border-0 bg-transparent py-2.5 px-3 text-base-100 focus:ring-0 sm:text-sm sm:leading-6 disabled:bg-base-950 placeholder:text-base-500 [&:-webkit-autofill]:!text-base-100 [&:-webkit-autofill]:[-webkit-text-fill-color:rgb(var(--color-base-100))]']) }}\n                   placeholder=\"{{ $placeholder ?? '' }}\">\n        </div>\n\n        @error($field) <span class=\"text-red text-sm mt-1\">{{ $message }}</span> @enderror\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/form/textarea.blade.php",
    "content": "@props(['field', 'name' => '', 'placeholder' => '', 'description' => '', 'rows' => 4, 'live' => true, 'xRef' => null])\n<div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n    @if(!blank($name))\n    <div class=\"flex flex-col justify-center\">\n        <label for=\"{{ $field }}\" class=\"block text-base font-semibold leading-6 text-base-50\">@lang($name)</label>\n        @if($description)\n            <span class=\"text-base-400 text-sm mt-1\">{{ $description }}</span>\n        @endif\n    </div>\n    @endif\n    <div class=\"flex flex-col justify-center\">\n        <div\n            class=\"flex rounded-lg bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red transition-all duration-200\">\n            <textarea\n                name=\"{{ $name }}\"\n                id=\"{{ $field }}\"\n                rows=\"{{ $rows }}\"\n                @if($live) wire:model.blur=\"{{ $field }}\" @else wire:model=\"{{ $field }}\" @endif\n                wire:loading.attr=\"disabled\"\n                placeholder=\"{{ $placeholder }}\"\n                @if($xRef) x-ref=\"{{ $xRef }}\" @endif\n                {{ $attributes->merge(['class' => 'flex-1 border-0 bg-transparent py-2.5 px-3 text-base-100 focus:ring-0 sm:text-sm sm:leading-6 disabled:bg-base-950 placeholder:text-base-500 resize-none font-mono']) }}></textarea>\n        </div>\n\n        @error($field) <span class=\"text-red text-sm mt-1\">{{ $message }}</span> @enderror\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/form/time.blade.php",
    "content": "@props(['field', 'name', 'description' => '', 'step' => '300'])\n<div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n    <div class=\"flex flex-col justify-center\">\n        <label for=\"{{ $field }}\" class=\"block text-base font-semibold leading-6 text-base-50\">@lang($name)</label>\n        @if($description)\n            <span class=\"text-base-400 text-sm mt-1\">{{ $description }}</span>\n        @endif\n    </div>\n    <div class=\"flex flex-col justify-center\">\n        <div\n            class=\"flex rounded-lg bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red transition-all duration-200\">\n            <input type=\"time\"\n                   name=\"{{ $name }}\"\n                   id=\"{{ $field }}\"\n                   wire:model.blur=\"{{ $field }}\"\n                   wire:loading.attr=\"disabled\"\n                   step=\"{{ $step }}\"\n                   {{ $attributes->merge(['class' => 'flex-1 border-0 bg-transparent py-2.5 px-3 text-base-100 focus:ring-0 sm:text-sm sm:leading-6 disabled:bg-base-950']) }}>\n        </div>\n\n        @error($field) <span class=\"text-red text-sm mt-1\">{{ $message }}</span> @enderror\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/form-section.blade.php",
    "content": "@props(['submit'])\n\n<div {{ $attributes->merge(['class' => 'md:grid md:grid-cols-3 md:gap-8']) }}>\n    <x-section-title>\n        <x-slot name=\"title\">{{ $title }}</x-slot>\n        <x-slot name=\"description\">{{ $description }}</x-slot>\n    </x-section-title>\n\n    <div class=\"mt-5 md:mt-0 md:col-span-2\">\n        <form wire:submit=\"{{ $submit }}\">\n            <div class=\"px-6 py-8 sm:p-8 bg-gradient-to-br from-base-850 to-base-900 border border-base-700 shadow-xl {{ isset($actions) ? 'sm:rounded-tl-xl sm:rounded-tr-xl' : 'sm:rounded-xl' }} relative overflow-hidden\">\n                <!-- Subtle gradient overlay -->\n                <div class=\"absolute inset-0 bg-gradient-to-b from-base-800/10 to-transparent pointer-events-none\"></div>\n                \n                <div class=\"grid grid-cols-6 gap-6 relative\">\n                    {{ $form }}\n                </div>\n            </div>\n\n            @if (isset($actions))\n                <div class=\"flex items-center justify-end gap-3 px-6 py-4 bg-base-900 border border-t-0 border-base-700 text-end shadow-xl sm:rounded-bl-xl sm:rounded-br-xl\">\n                    {{ $actions }}\n                </div>\n            @endif\n        </form>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/input-error.blade.php",
    "content": "@props(['for'])\n\n@error($for)\n    <p {{ $attributes->merge(['class' => 'text-sm text-red-600']) }}>{{ $message }}</p>\n@enderror\n"
  },
  {
    "path": "resources/views/components/input.blade.php",
    "content": "@props(['disabled' => false])\n\n<div\n    class=\"flex rounded-lg bg-base-900 ring-1 ring-inset ring-base-700 focus-within:ring-2 focus-within:ring-inset focus-within:ring-red transition-all duration-200\">\n    <input {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'flex-1 border-0 bg-transparent py-2.5 px-3 text-base-100 focus:ring-0 sm:text-sm sm:leading-6 placeholder:text-base-500']) !!}>\n</div>\n"
  },
  {
    "path": "resources/views/components/label.blade.php",
    "content": "@props(['value'])\n\n<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-base-100']) }}>\n    {{ $value ?? $slot }}\n</label>\n"
  },
  {
    "path": "resources/views/components/layout/sidebar/menu.blade.php",
    "content": "<div class=\"hidden h-full lg:inset-y-0 lg:z-40 lg:flex lg:w-72 lg:flex-col relative overflow-hidden\">\n    <!-- Floating orbs -->\n    <div class=\"absolute right-4 top-24 w-2 h-2 rounded-full bg-blue blur-sm pointer-events-none opacity-50 animate-float\"></div>\n    <div class=\"absolute right-6 top-64 w-1.5 h-1.5 rounded-full bg-indigo blur-sm pointer-events-none opacity-40 animate-float\" style=\"animation-delay: -2s;\"></div>\n    <div class=\"absolute right-3 bottom-32 w-2.5 h-2.5 rounded-full bg-blue-light blur-sm pointer-events-none opacity-50 animate-float\" style=\"animation-delay: -4s;\"></div>\n    \n    <div class=\"flex grow flex-col gap-y-6 overflow-y-auto overflow-x-hidden dark:bg-base-black px-6 pb-4 relative\">\n        <!-- Radial glow emanating from edge -->\n        <div class=\"absolute -right-32 top-2/3 w-64 h-64 bg-blue/10 rounded-full blur-3xl pointer-events-none animate-pulse-glow\" style=\"animation-delay: -2s;\"></div>\n        \n        <!-- Logo with subtle glow -->\n        <div class=\"flex h-16 shrink-0 items-center pt-4\">\n            <a href=\"/\" class=\"inline-block transition-transform duration-300 hover:scale-105\">\n                <img class=\"h-14 w-auto\"\n                    src=\"{{ url('img/logo.svg') }}\" alt=\"{{ config('app.name') }}\">\n            </a>\n        </div>\n\n        <nav class=\"flex flex-1 flex-col\">\n            <ul role=\"list\" class=\"flex flex-1 flex-col gap-y-7\">\n                <li>\n                    <ul role=\"list\" class=\"-mx-2 space-y-1.5\">\n                        @foreach (\\Vigilant\\Core\\Facades\\Navigation::items() as $item)\n                            @continue(!$item->shouldRender())\n                            @php\n                                $activeChild = false;\n                                if ($item->hasChildren()) {\n                                    foreach ($item->getChildren() as $child) {\n                                        if ($child->active()) {\n                                            $activeChild = true;\n                                            break;\n                                        }\n                                    }\n                                }\n                            @endphp\n                            <li x-data=\"{ showChildren: {{ $item->active() || $activeChild ? 'true' : 'false' }} }\">\n                                <a @if ($item->url !== null) href=\"{{ $item->url }}\" wire:navigate.hover\n                                @else\n                                    href=\"#\" x-on:click.prevent=\"showChildren = !showChildren\" @endif\n                                    @class([\n                                        'group relative flex items-center gap-x-3 rounded-lg p-2.5 text-sm leading-6 font-semibold transition-all duration-200',\n                                        'bg-gradient-to-r from-red/10 to-transparent text-base-50 shadow-sm shadow-red/5' =>\n                                            $item->active() || $activeChild,\n                                        'text-base-300 hover:text-base-50 hover:bg-base-900/50' => !$item->active(),\n                                    ])>\n                                    @if ($item->active() || $activeChild)\n                                        <div\n                                            class=\"absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-gradient-to-b from-red to-orange rounded-r-full\">\n                                        </div>\n                                    @endif\n\n                                    @if ($item->icon !== null)\n                                        <div\n                                            class=\"flex items-center justify-center w-8 h-8 rounded-lg transition-all duration-200 {{ $item->active() || $activeChild ? 'bg-red/20' : 'group-hover:bg-base-800' }}\">\n                                            @svg($item->icon, 'h-5 w-5 shrink-0 transition-all duration-200 ' . ($item->active() || $activeChild ? 'text-red' : 'text-base-400 group-hover:text-base-200'))\n                                        </div>\n                                    @endif\n                                    <span class=\"flex-1\">{{ __($item->name) }}</span>\n\n                                    @if ($item->hasChildren() || $activeChild)\n                                        <div class=\"transition-transform duration-200\"\n                                            x-bind:class=\"showChildren ? 'rotate-180' : ''\">\n                                            @svg('heroicon-o-chevron-down', 'h-4 w-4')\n                                        </div>\n                                    @endif\n                                </a>\n\n                                @if ($item->hasChildren())\n                                    <ul class=\"mt-1.5 ml-11 space-y-1 overflow-hidden\" x-show=\"showChildren\" x-cloak\n                                        x-collapse>\n                                        @foreach ($item->getChildren() as $child)\n                                            <li\n                                                class=\"relative pl-4 border-l-2 border-base-800 hover:border-red/30 transition-colors duration-200\">\n                                                <a href=\"{{ $child->url }}\" wire:navigate.hover\n                                                    @class([\n                                                        'group flex items-center gap-x-2 rounded-lg p-2 text-sm leading-6 font-medium transition-all duration-200',\n                                                        'text-base-50 bg-base-900/30' => $child->active(),\n                                                        'text-base-400 hover:text-base-50 hover:bg-base-900/30' => !$child->active(),\n                                                    ])>\n                                                    @if ($child->active())\n                                                        <div\n                                                            class=\"absolute -left-[2px] top-1/2 -translate-y-1/2 w-0.5 h-6 bg-red rounded-r-full\">\n                                                        </div>\n                                                    @endif\n\n                                                    @if ($child->icon !== null)\n                                                        @svg($child->icon, 'h-4 w-4 shrink-0 transition-colors duration-200 ' . ($child->active() ? 'text-red' : 'text-base-500 group-hover:text-base-300'))\n                                                    @else\n                                                        <div\n                                                            class=\"w-1.5 h-1.5 rounded-full {{ $child->active() ? 'bg-red' : 'bg-base-600 group-hover:bg-base-400' }} transition-colors duration-200\">\n                                                        </div>\n                                                    @endif\n                                                    {{ __($child->name) }}\n                                                </a>\n                                            </li>\n                                        @endforeach\n                                    </ul>\n                                @endif\n                            </li>\n                        @endforeach\n                    </ul>\n                </li>\n\n                <!-- Bottom section with visual card -->\n                <li class=\"mt-auto\">\n                    <div class=\"mb-4 h-px bg-gradient-to-r from-transparent via-base-700 to-transparent\"></div>\n                    \n                    <x-layout.user-profile-dropdown />\n                </li>\n            </ul>\n        </nav>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/layout/sidebar/mobile-menu.blade.php",
    "content": "<div class=\"relative z-50 lg:hidden\" role=\"dialog\" aria-modal=\"true\" x-cloak x-show=\"sidebarOpen\">\n    <!-- Backdrop with proper opacity -->\n    <div class=\"fixed inset-0 bg-base-black/80 backdrop-blur-sm\" x-show=\"sidebarOpen\"\n        x-transition:enter=\"transition-opacity ease-linear duration-300\" \n        x-transition:enter-start=\"opacity-0\"\n        x-transition:enter-end=\"opacity-100\" \n        x-transition:leave=\"transition-opacity ease-linear duration-300\"\n        x-transition:leave-start=\"opacity-100\" \n        x-transition:leave-end=\"opacity-0\"></div>\n\n    <div class=\"fixed inset-0 flex\">\n        <div class=\"relative mr-16 flex w-full max-w-xs flex-1\" x-show=\"sidebarOpen\"\n            x-on:click.outside=\"sidebarOpen = false\" \n            x-transition:enter=\"transition ease-in-out duration-300 transform\"\n            x-transition:enter-start=\"-translate-x-full\" \n            x-transition:enter-end=\"translate-x-0\"\n            x-transition:leave=\"transition ease-in-out duration-300 transform\" \n            x-transition:leave-start=\"translate-x-0\"\n            x-transition:leave-end=\"-translate-x-full\">\n\n            <!-- Close button -->\n            <div class=\"absolute left-full top-0 flex w-16 justify-center pt-5\"\n                x-transition:enter=\"ease-in-out duration-300\" \n                x-transition:enter-start=\"opacity-0\"\n                x-transition:enter-end=\"opacity-100\" \n                x-transition:leave=\"ease-in-out duration-300\"\n                x-transition:leave-start=\"opacity-100\" \n                x-transition:leave-end=\"opacity-0\">\n                <button type=\"button\" class=\"group -m-2 p-2 rounded-lg hover:bg-base-900/50 transition-all duration-200\" x-on:click=\"sidebarOpen = false\">\n                    <span class=\"sr-only\">Close sidebar</span>\n                    <svg class=\"h-6 w-6 text-base-300 group-hover:text-base-100 transition-colors duration-200\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\"\n                        stroke=\"currentColor\" aria-hidden=\"true\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n                    </svg>\n                </button>\n            </div>\n\n            <!-- Sidebar panel -->\n            <div class=\"flex grow flex-col gap-y-5 overflow-y-auto bg-base-950 px-6 pb-4 border-r border-base-800/50\">\n                <!-- Logo -->\n                <div class=\"flex h-16 shrink-0 items-center pt-4\">\n                    <a href=\"/\" wire:navigate.hover class=\"group\">\n                        <img class=\"h-16 w-auto transition-transform duration-300 group-hover:scale-105\" src=\"{{ url('img/logo.svg') }}\" alt=\"{{ config('app.name') }}\">\n                    </a>\n                </div>\n\n                <!-- Navigation -->\n                <nav class=\"flex flex-1 flex-col\">\n                    <ul role=\"list\" class=\"flex flex-1 flex-col gap-y-7\">\n                        <li>\n                            <ul role=\"list\" class=\"-mx-2 space-y-1\">\n                                @foreach (\\Vigilant\\Core\\Facades\\Navigation::items() as $item)\n                                    @continue(!$item->shouldRender())\n                                    <li>\n                                        <a href=\"{{ $item->url }}\" wire:navigate.hover @class([\n                                            'group flex gap-x-3 rounded-lg px-3 py-2.5 text-sm font-semibold transition-all duration-200',\n                                            'bg-base-900/50 text-base-100 border border-base-800/50' => $item->active(),\n                                            'text-base-400 hover:text-base-100 hover:bg-base-900/30' => !$item->active(),\n                                        ])>\n                                            @if ($item->icon !== null)\n                                                @svg($item->icon, 'h-5 w-5 shrink-0 transition-colors duration-200' . ($item->active() ? ' text-red' : ' text-base-500 group-hover:text-base-300'))\n                                            @endif\n                                            {{ __($item->name) }}\n                                        </a>\n\n                                        @if ($item->hasChildren())\n                                            <ul class=\"mt-2 ml-3 pl-4 border-l-2 border-red/30 space-y-1\">\n                                                @foreach ($item->getChildren() as $child)\n                                                    <li>\n                                                        <a href=\"{{ $child->url }}\" wire:navigate.hover\n                                                            @class([\n                                                                'group flex gap-x-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200',\n                                                                'text-base-100' => $child->active(),\n                                                                'text-base-400 hover:text-base-100 hover:bg-base-900/30' => !$child->active(),\n                                                            ])>\n                                                            <span class=\"flex items-center gap-x-2\">\n                                                                @if ($child->icon !== null)\n                                                                    @svg($child->icon, 'h-4 w-4 shrink-0 transition-colors duration-200' . ($child->active() ? ' text-red' : ' text-base-500 group-hover:text-base-300'))\n                                                                @endif\n                                                                {{ __($child->name) }}\n                                                            </span>\n                                                        </a>\n                                                    </li>\n                                                @endforeach\n                                            </ul>\n                                        @endif\n                                    </li>\n                                @endforeach\n                            </ul>\n                        </li>\n\n                        <!-- User Profile at bottom -->\n                        <li class=\"mt-auto\">\n                            <!-- Gradient divider -->\n                            <div class=\"mb-4 h-px bg-gradient-to-r from-transparent via-base-700 to-transparent\"></div>\n                            \n                            <x-layout.user-profile-dropdown />\n                        </li>\n                    </ul>\n                </nav>\n            </div>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/layout/sidebar.blade.php",
    "content": "<div>\n    <x-layout.sidebar.mobile-menu />\n    <x-layout.sidebar.menu />\n</div>\n"
  },
  {
    "path": "resources/views/components/layout/topbar.blade.php",
    "content": "<div class=\"sticky top-0 z-20 flex h-12 sm:h-14 shrink-0 items-center gap-x-3 bg-base-black px-4 sm:px-6 lg:px-8\">\n    <!-- Mobile menu button -->\n    <button type=\"button\" class=\"group -m-2 p-2 text-base-400 hover:text-base-200 transition-colors duration-200 lg:hidden\" x-on:click=\"sidebarOpen = true\">\n        <span class=\"sr-only\">Open sidebar</span>\n        <svg class=\"h-5 w-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5\" />\n        </svg>\n    </button>\n\n    <div class=\"flex flex-1 gap-x-3 self-stretch\">\n        <div class=\"flex-1\"></div>\n        <div class=\"flex items-center gap-x-2\">\n            <!-- Team Switcher -->\n            @if (Laravel\\Jetstream\\Jetstream::hasTeamFeatures() && Auth::user() !== null)\n                <div class=\"relative\" x-data=\"{ open: false }\">\n                    <button type=\"button\" \n                        class=\"group flex items-center gap-x-2 rounded-lg px-2.5 py-1.5 text-sm font-semibold bg-base-900/50 border border-base-800/50 text-base-100 hover:bg-base-800/50 hover:border-base-700 transition-all duration-200\"\n                        x-on:click=\"open = !open\">\n                        <!-- Team icon -->\n                        <svg class=\"h-4 w-4 text-base-400 group-hover:text-base-200 transition-colors duration-200\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n                            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z\" />\n                        </svg>\n                        <span class=\"truncate max-w-[150px]\">{{ Auth::user()?->currentTeam?->name ?? '-' }}</span>\n                        <svg class=\"h-3.5 w-3.5 text-base-400 group-hover:text-base-200 transition-colors duration-200\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n                            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9\" />\n                        </svg>\n                    </button>\n\n                    <div x-show=\"open\" \n                         x-on:click.outside=\"open = false\" \n                         x-cloak\n                         x-transition:enter=\"transition ease-out duration-100\"\n                         x-transition:enter-start=\"transform opacity-0 scale-95\"\n                         x-transition:enter-end=\"transform opacity-100 scale-100\"\n                         x-transition:leave=\"transition ease-in duration-75\"\n                         x-transition:leave-start=\"transform opacity-100 scale-100\"\n                         x-transition:leave-end=\"transform opacity-0 scale-95\"\n                         class=\"absolute right-0 z-10 mt-2 w-60 origin-top-right rounded-lg bg-base-950 border border-base-800/50 shadow-lg overflow-hidden\"\n                         role=\"menu\">\n                        <!-- Header -->\n                        <div class=\"px-4 py-3 border-b border-base-800/50 bg-base-900/30\">\n                            <p class=\"text-xs font-semibold text-base-400 uppercase tracking-wider\">{{ __('Manage Team') }}</p>\n                        </div>\n\n                        <!-- Team Settings -->\n                        <div class=\"py-1\">\n                            <a href=\"{{ route('settings', ['tab' => 'team']) }}\" \n                               class=\"group flex items-center gap-2 px-4 py-2.5 text-sm font-medium text-base-300 hover:text-base-50 hover:bg-base-900/50 transition-all duration-200\">\n                                <div class=\"flex items-center justify-center w-7 h-7 rounded-lg transition-all duration-200 group-hover:bg-base-800\">\n                                    <svg class=\"h-4 w-4 text-base-500 group-hover:text-base-200 transition-colors duration-200\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n                                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z\" />\n                                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15 12a3 3 0 11-6 0 3 3 0 016 0z\" />\n                                    </svg>\n                                </div>\n                                {{ __('Team Settings') }}\n                            </a>\n\n                            @can('create', Laravel\\Jetstream\\Jetstream::newTeamModel())\n                                <a href=\"{{ route('teams.create') }}\" \n                                   class=\"group flex items-center gap-2 px-4 py-2.5 text-sm font-medium text-base-300 hover:text-base-50 hover:bg-base-900/50 transition-all duration-200\">\n                                    <div class=\"flex items-center justify-center w-7 h-7 rounded-lg transition-all duration-200 group-hover:bg-base-800\">\n                                        <svg class=\"h-4 w-4 text-base-500 group-hover:text-base-200 transition-colors duration-200\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n                                            <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 4.5v15m7.5-7.5h-15\" />\n                                        </svg>\n                                    </div>\n                                    {{ __('Create New Team') }}\n                                </a>\n                            @endcan\n                        </div>\n\n                        @if (Auth::user()->allTeams()->count() > 1)\n                            <!-- Switch Teams Section -->\n                            <div class=\"border-t border-base-800/50\">\n                                <div class=\"px-4 py-3 bg-base-900/30\">\n                                    <p class=\"text-xs font-semibold text-base-400 uppercase tracking-wider\">{{ __('Switch Teams') }}</p>\n                                </div>\n                                <div class=\"py-1\">\n                                    @foreach (Auth::user()->allTeams() as $team)\n                                        <x-switchable-team :team=\"$team\" />\n                                    @endforeach\n                                </div>\n                            </div>\n                        @endif\n                    </div>\n                </div>\n            @endif\n\n            <!-- Notifications -->\n            <a href=\"{{ route('notifications.history') }}\" \n               class=\"notification-bell group relative p-2 rounded-lg text-base-400 hover:text-red hover:bg-base-900/50 transition-all duration-200\">\n                <span class=\"sr-only\">@lang('View notifications')</span>\n                <svg class=\"h-5 w-5 transition-colors duration-200\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0\" />\n                </svg>\n            </a>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/layout/user-profile-dropdown.blade.php",
    "content": "<!-- User profile card -->\n<div class=\"mb-3 p-3 rounded-xl bg-gradient-to-br from-base-900 to-base-950 border border-base-800/50 shadow-lg\" x-data=\"{ open: false }\">\n    <button type=\"button\"\n        class=\"group w-full flex items-center gap-3 transition-all duration-200\"\n        x-on:click=\"open = !open\">\n        <!-- Avatar -->\n        <div class=\"relative flex-shrink-0\">\n            <div class=\"w-10 h-10 rounded-lg bg-gradient-to-br from-red/20 to-orange/20 border border-red/30 flex items-center justify-center overflow-hidden group-hover:border-red/50 transition-colors duration-200\">\n                <svg class=\"h-6 w-6 text-red\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z\" />\n                </svg>\n            </div>\n            <!-- Online indicator -->\n            <div class=\"absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green rounded-full border-2 border-base-900\"></div>\n        </div>\n        \n        <!-- User info -->\n        <div class=\"flex-1 text-left min-w-0\">\n            <p class=\"text-sm font-semibold text-base-50 truncate\">{{ auth()->user()->name ?? __('User') }}</p>\n            <p class=\"text-xs text-base-400 truncate\">{{ auth()->user()->email ?? '' }}</p>\n        </div>\n        \n        <!-- Chevron -->\n        <div class=\"transition-transform duration-200 flex-shrink-0\" x-bind:class=\"open ? 'rotate-180' : ''\">\n            <svg class=\"h-4 w-4 text-base-400 group-hover:text-base-200 transition-colors duration-200\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n                <path fill-rule=\"evenodd\" d=\"M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z\" clip-rule=\"evenodd\" />\n            </svg>\n        </div>\n    </button>\n    \n    <!-- Dropdown menu -->\n    <div x-show=\"open\" \n         x-cloak \n         x-collapse\n         class=\"mt-3 pt-3 border-t border-base-800/50 space-y-1 overflow-hidden\">\n        <!-- Settings link -->\n        <a href=\"{{ route('settings') }}\" wire:navigate.hover\n            @class([\n                'group flex items-center gap-2 rounded-lg p-2 text-sm font-medium transition-all duration-200',\n                'bg-red/10 text-base-50' => Route::currentRouteName() === 'settings',\n                'text-base-300 hover:text-base-50 hover:bg-base-800/50' => Route::currentRouteName() !== 'settings',\n            ])>\n            <div class=\"flex items-center justify-center w-7 h-7 rounded-lg transition-all duration-200 {{ Route::currentRouteName() === 'settings' ? 'bg-red/20' : 'group-hover:bg-base-800' }}\">\n                <svg class=\"h-4 w-4 transition-colors duration-200 {{ Route::currentRouteName() === 'settings' ? 'text-red' : 'text-base-400 group-hover:text-base-200' }}\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z\" />\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15 12a3 3 0 11-6 0 3 3 0 016 0z\" />\n                </svg>\n            </div>\n            @lang('Settings')\n        </a>\n        \n        <!-- Logout -->\n        <form action=\"{{ route('logout') }}\" method=\"POST\" class=\"w-full\">\n            @csrf\n            <button type=\"submit\"\n                class=\"group w-full flex items-center gap-2 rounded-lg p-2 text-sm font-medium text-base-300 hover:text-base-50 hover:bg-base-800/50 transition-all duration-200\">\n                <div class=\"flex items-center justify-center w-7 h-7 rounded-lg transition-all duration-200 group-hover:bg-base-800\">\n                    <svg class=\"h-4 w-4 text-base-400 group-hover:text-base-200 transition-colors duration-200\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75\" />\n                    </svg>\n                </div>\n                @lang('Sign out')\n            </button>\n        </form>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/modal.blade.php",
    "content": "@props(['id', 'maxWidth'])\n\n@php\n$id = $id ?? md5($attributes->wire('model'));\n\n$maxWidth = [\n    'sm' => 'sm:max-w-sm',\n    'md' => 'sm:max-w-md',\n    'lg' => 'sm:max-w-lg',\n    'xl' => 'sm:max-w-xl',\n    '2xl' => 'sm:max-w-2xl',\n][$maxWidth ?? '2xl'];\n@endphp\n\n<div\n    x-data=\"{ show: @entangle($attributes->wire('model')) }\"\n    x-on:close.stop=\"show = false\"\n    x-on:keydown.escape.window=\"show = false\"\n    x-show=\"show\"\n    id=\"{{ $id }}\"\n    class=\"jetstream-modal fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50 sm:flex sm:items-center sm:justify-center\"\n    style=\"display: none;\"\n>\n    <div x-show=\"show\" class=\"fixed inset-0 transform transition-all z-40\" x-on:click=\"show = false\" x-transition:enter=\"ease-out duration-300\"\n                    x-transition:enter-start=\"opacity-0\"\n                    x-transition:enter-end=\"opacity-100\"\n                    x-transition:leave=\"ease-in duration-200\"\n                    x-transition:leave-start=\"opacity-100\"\n                    x-transition:leave-end=\"opacity-0\">\n        <div class=\"absolute inset-0 bg-base-950/85 backdrop-blur-sm\"></div>\n    </div>\n\n    <div x-show=\"show\" class=\"relative z-50 mb-6 bg-base-900/95 border border-base-800 rounded-2xl overflow-hidden shadow-2xl shadow-black/40 ring-1 ring-base-800/70 transform transition-all pointer-events-auto sm:w-full {{ $maxWidth }} sm:mx-auto\"\n                    x-trap.inert.noscroll=\"show\"\n                    x-transition:enter=\"ease-out duration-300\"\n                    x-transition:enter-start=\"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\"\n                    x-transition:enter-end=\"opacity-100 translate-y-0 sm:scale-100\"\n                    x-transition:leave=\"ease-in duration-200\"\n                    x-transition:leave-start=\"opacity-100 translate-y-0 sm:scale-100\"\n                    x-transition:leave-end=\"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\">\n        {{ $slot }}\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/nav-link.blade.php",
    "content": "@props(['active'])\n\n@php\n$classes = ($active ?? false)\n            ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo text-sm font-medium leading-5 text-base-50 focus:outline-hidden focus:border-indigo-light transition duration-150 ease-in-out'\n            : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-base-400 hover:text-base-200 hover:border-base-700 focus:outline-hidden focus:text-base-200 focus:border-base-700 transition duration-150 ease-in-out';\n@endphp\n\n<a {{ $attributes->merge(['class' => $classes]) }}>\n    {{ $slot }}\n</a>\n"
  },
  {
    "path": "resources/views/components/page-header.blade.php",
    "content": "@props(['title', 'back'])\n@section('title', $title)\n\n<div x-data=\"{ show: true }\" \n     @navigation-start.window=\"show = false\" \n     @navigation-end.window=\"show = true\"\n     x-transition:enter=\"transition ease-out duration-300\"\n     x-transition:enter-start=\"opacity-0\"\n     x-transition:enter-end=\"opacity-100\"\n     x-transition:leave=\"transition ease-in duration-200\"\n     x-transition:leave-start=\"opacity-100\"\n     x-transition:leave-end=\"opacity-0\"\n     :style=\"show ? '' : 'opacity: 0'\"\n     {{ $attributes->merge(['class' => 'relative mb-4 sm:mb-6']) }}>\n    <!-- Subtle background glow with noise to prevent banding -->\n    <div class=\"absolute -inset-x-4 -inset-y-2 blur-xl -z-10\" \n         style=\"background: \n                linear-gradient(90deg, \n                    rgba(239, 68, 68, 0.03) 0%, \n                    rgba(239, 68, 68, 0.025) 10%,\n                    rgba(239, 68, 68, 0.02) 20%,\n                    rgba(239, 68, 68, 0.015) 30%,\n                    rgba(239, 68, 68, 0.01) 40%,\n                    rgba(239, 68, 68, 0.005) 45%,\n                    transparent 50%,\n                    rgba(59, 130, 246, 0.005) 55%,\n                    rgba(59, 130, 246, 0.01) 60%,\n                    rgba(59, 130, 246, 0.015) 70%,\n                    rgba(59, 130, 246, 0.02) 80%,\n                    rgba(59, 130, 246, 0.025) 90%,\n                    rgba(59, 130, 246, 0.03) 100%\n                ),\n                url('data:image/svg+xml,%3Csvg viewBox=%220 0 256 256%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noiseFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%221.2%22 numOctaves=%225%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noiseFilter)%22 opacity=%220.15%22/%3E%3C/svg%3E');\">\n    </div>\n\n    <div class=\"flex items-center justify-between gap-3 sm:gap-4\">\n        <div class=\"min-w-0 flex items-center gap-3 sm:gap-4 flex-1\">\n            @if (isset($back))\n                <a href=\"{{ $back }}\" wire:navigate.hover\n                    class=\"flex-shrink-0 relative flex items-center justify-center w-9 h-9 sm:w-10 sm:h-10 rounded-lg bg-base-850 border border-base-700 text-base-300 hover:text-base-50 hover:bg-base-800 hover:border-indigo transition-all duration-300 group overflow-hidden shadow-lg hover:shadow-indigo/20\">\n                    <div class=\"absolute inset-0 bg-gradient-to-br from-indigo/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300\"></div>\n                    @svg('tni-arrow-left-circle-o', 'w-4 h-4 sm:w-5 sm:h-5 relative z-10 group-hover:-translate-x-0.5 transition-transform duration-200')\n                </a>\n            @endif\n            <div class=\"min-w-0 flex-1\">\n                <h1\n                    class=\"text-xl sm:text-2xl md:text-3xl font-bold leading-tight bg-gradient-to-r from-base-50 via-base-100 to-base-200 bg-clip-text text-transparent truncate\">\n                    {{ __($title) }}\n                </h1>\n                <div class=\"h-0.5 sm:h-1 w-12 sm:w-16 rounded-full mt-1 sm:mt-1.5 relative overflow-hidden\">\n                    <div class=\"absolute inset-0 bg-gradient-to-r from-red via-orange to-transparent\"></div>\n                    <div class=\"absolute inset-0 opacity-30 mix-blend-soft-light\" \n                         style=\"background-image: url('data:image/svg+xml,%3Csvg viewBox=%220 0 256 256%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noise%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%221%22 numOctaves=%224%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noise)%22/%3E%3C/svg%3E');\"></div>\n                </div>\n            </div>\n        </div>\n        <div class=\"flex items-center gap-2 sm:gap-3 flex-shrink-0\">\n            {{ $slot }}\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/password-group.blade.php",
    "content": "@props([\n    'passwordName' => 'password',\n    'passwordConfirmationName' => null,\n    'passwordId' => null,\n    'passwordConfirmationId' => null,\n    'passwordLabel' => null,\n    'passwordConfirmationLabel' => null,\n    'passwordPlaceholder' => '••••••••',\n    'passwordConfirmationPlaceholder' => null,\n    'passwordAutocomplete' => 'new-password',\n    'passwordConfirmationAutocomplete' => 'new-password',\n    'passwordModel' => null,\n    'passwordConfirmationModel' => null,\n])\n\n@php\n    $passwordConfirmationName ??= $passwordName . '_confirmation';\n    $passwordId ??= $passwordName;\n    $passwordConfirmationId ??= $passwordConfirmationName;\n    $passwordLabel ??= __('Password');\n    $passwordConfirmationLabel ??= __('Confirm Password');\n    $passwordConfirmationPlaceholder ??= __('Confirm password');\n@endphp\n\n<div {{ $attributes->class('space-y-6') }}\n    x-data=\"{\n        showPasswordStrength: false,\n        passwordStrength: 0,\n        passwordValue: '',\n        confirmationMatches: false,\n        updateStrength(value) {\n            this.passwordValue = value ?? '';\n            let strength = 0;\n            if (this.passwordValue.length >= 8) strength++;\n            if (/[a-z]/.test(this.passwordValue) && /[A-Z]/.test(this.passwordValue)) strength++;\n            if (/\\d/.test(this.passwordValue)) strength++;\n            if (/[^a-zA-Z\\d]/.test(this.passwordValue)) strength++;\n            this.passwordStrength = strength;\n            this.updateMatch();\n        },\n        updateMatch(value = null) {\n            const confirmValue = value ?? (this.$refs.passwordConfirm?.value ?? '');\n            this.confirmationMatches = confirmValue.length > 0 && confirmValue === this.passwordValue;\n        }\n    }\">\n    <div>\n        <label for=\"{{ $passwordId }}\" class=\"text-xs font-semibold uppercase tracking-[0.35em] text-base-300\">\n            {{ $passwordLabel }}\n        </label>\n        <div class=\"relative mt-3\">\n            <input\n                id=\"{{ $passwordId }}\"\n                name=\"{{ $passwordName }}\"\n                type=\"password\"\n                placeholder=\"{{ $passwordPlaceholder }}\"\n                autocomplete=\"{{ $passwordAutocomplete }}\"\n                @focus=\"showPasswordStrength = true\"\n                @input=\"updateStrength($event.target.value)\"\n                @if ($passwordModel) wire:model.defer=\"{{ $passwordModel }}\" @endif\n                class=\"w-full rounded-2xl border border-base-700 bg-base-black/40 px-5 py-4 text-base text-base-50 placeholder:text-base-500 focus:border-indigo focus:ring-2 focus:ring-indigo/40\"\n            />\n        </div>\n\n        <div class=\"mt-3 space-y-2 overflow-hidden\" x-show=\"showPasswordStrength\"\n            x-transition:enter=\"transition ease-out duration-250\"\n            x-transition:enter-start=\"opacity-0 -translate-y-2\"\n            x-transition:enter-end=\"opacity-100 translate-y-0\">\n            <div class=\"flex gap-1.5\">\n                <template x-for=\"index in 4\" :key=\"index\">\n                    <div class=\"h-1.5 flex-1 rounded-full bg-base-800 overflow-hidden transition-all duration-300\"\n                        :class=\"passwordStrength >= index && ['bg-gradient-to-r from-red to-orange','bg-gradient-to-r from-orange to-yellow','bg-gradient-to-r from-yellow to-green-light','bg-gradient-to-r from-green to-green-light'][index-1]\">\n                    </div>\n                </template>\n            </div>\n            <p class=\"text-xs transition-all duration-200\"\n                :class=\"{\n                    'text-red': passwordStrength <= 1,\n                    'text-orange': passwordStrength === 2,\n                    'text-yellow': passwordStrength === 3,\n                    'text-green-light': passwordStrength === 4\n                }\">\n                <span x-show=\"passwordStrength === 0\">{{ __('Enter a password') }}</span>\n                <span x-show=\"passwordStrength === 1\">{{ __('Weak password') }}</span>\n                <span x-show=\"passwordStrength === 2\">{{ __('Fair password') }}</span>\n                <span x-show=\"passwordStrength === 3\">{{ __('Good password') }}</span>\n                <span x-show=\"passwordStrength === 4\">{{ __('Strong password! 🎉') }}</span>\n            </p>\n        </div>\n\n        @error($passwordName)\n            <p class=\"mt-2 text-sm text-red\">{{ $message }}</p>\n        @enderror\n    </div>\n\n    <div>\n        <label for=\"{{ $passwordConfirmationId }}\" class=\"text-xs font-semibold uppercase tracking-[0.35em] text-base-300\">\n            {{ $passwordConfirmationLabel }}\n        </label>\n        <div class=\"relative mt-3\">\n            <input\n                id=\"{{ $passwordConfirmationId }}\"\n                name=\"{{ $passwordConfirmationName }}\"\n                type=\"password\"\n                placeholder=\"{{ $passwordConfirmationPlaceholder }}\"\n                autocomplete=\"{{ $passwordConfirmationAutocomplete }}\"\n                x-ref=\"passwordConfirm\"\n                @input=\"updateMatch($event.target.value)\"\n                @if ($passwordConfirmationModel) wire:model.defer=\"{{ $passwordConfirmationModel }}\" @endif\n                class=\"w-full rounded-2xl border border-base-700 bg-base-black/40 px-5 py-4 text-base text-base-50 placeholder:text-base-500 focus:border-indigo focus:ring-2 focus:ring-indigo/40\"\n            />\n            <div class=\"absolute right-3 top-1/2 -translate-y-1/2 opacity-0 scale-0 transition-all duration-200\"\n                :class=\"confirmationMatches && 'opacity-100 scale-100'\">\n                <svg class=\"w-5 h-5 text-green\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\" />\n                </svg>\n            </div>\n        </div>\n        @error($passwordConfirmationName)\n            <p class=\"mt-2 text-sm text-red\">{{ $message }}</p>\n        @enderror\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/responsive-nav-link.blade.php",
    "content": "@props(['active'])\n\n@php\n$classes = ($active ?? false)\n            ? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo text-start text-base font-medium text-indigo-light bg-base-900 focus:outline-hidden focus:text-indigo-light focus:bg-base-850 focus:border-indigo transition duration-150 ease-in-out'\n            : 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-base-400 hover:text-base-200 hover:bg-base-900 hover:border-base-700 focus:outline-hidden focus:text-base-200 focus:bg-base-900 focus:border-base-700 transition duration-150 ease-in-out';\n@endphp\n\n<a {{ $attributes->merge(['class' => $classes]) }}>\n    {{ $slot }}\n</a>\n"
  },
  {
    "path": "resources/views/components/secondary-button.blade.php",
    "content": "<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center gap-2 px-6 py-2.5 bg-base-850 dark:bg-base-850 border border-base-700 dark:border-base-700 rounded-lg font-semibold text-sm text-base-200 dark:text-base-200 shadow-md hover:bg-base-800 dark:hover:bg-base-800 hover:shadow-lg hover:-translate-y-0.5 focus:outline-hidden focus:ring-2 focus:ring-red focus:ring-offset-2 dark:focus:ring-offset-base-black disabled:opacity-25 transition-all duration-200']) }}>\n    {{ $slot }}\n</button>\n"
  },
  {
    "path": "resources/views/components/section-border.blade.php",
    "content": "<div class=\"hidden sm:block\">\n    <div class=\"py-4\">\n        <div class=\"border-t border-base-700\"></div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/section-title.blade.php",
    "content": "<div class=\"md:col-span-1 flex justify-between\">\n    <div class=\"px-4 sm:px-0\">\n        <h3 class=\"text-xl font-bold text-base-50 bg-gradient-to-r from-base-50 via-base-100 to-base-200 bg-clip-text text-transparent\">{{ $title }}</h3>\n\n        <p class=\"mt-2 text-sm leading-relaxed text-base-300\">\n            {{ $description }}\n        </p>\n    </div>\n\n    <div class=\"px-4 sm:px-0\">\n        {{ $aside ?? '' }}\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/components/switchable-team.blade.php",
    "content": "@props(['team', 'component' => 'dropdown-link'])\n\n<form method=\"POST\" action=\"{{ route('current-team.update') }}\" x-data>\n    @method('PUT')\n    @csrf\n\n    <!-- Hidden Team ID -->\n    <input type=\"hidden\" name=\"team_id\" value=\"{{ $team->id }}\">\n\n    <x-dynamic-component :component=\"$component\" href=\"#\" x-on:click.prevent=\"$root.submit();\">\n        <div class=\"flex items-center\">\n            @if (Auth::user()->isCurrentTeam($team))\n                <svg class=\"me-2 h-5 w-5 text-green-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n                </svg>\n            @endif\n\n            <div class=\"truncate\">{{ $team->name }}</div>\n        </div>\n    </x-dynamic-component>\n</form>\n"
  },
  {
    "path": "resources/views/components/validation-errors.blade.php",
    "content": "@if ($errors->any())\n    <div {{ $attributes->merge(['class' => 'bg-red/10 border border-red/30 rounded-lg px-4 py-3']) }}>\n        <div class=\"font-medium text-red-light\">{{ __('Whoops! Something went wrong.') }}</div>\n\n        <ul class=\"mt-3 list-disc list-inside text-sm text-red\">\n            @foreach ($errors->all() as $error)\n                <li>{{ $error }}</li>\n            @endforeach\n        </ul>\n    </div>\n@endif\n"
  },
  {
    "path": "resources/views/dashboard.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header title=\"Dashboard\">\n\n        </x-page-header>\n    </x-slot>\n\n\n</x-app-layout>\n"
  },
  {
    "path": "resources/views/emails/team-invitation.blade.php",
    "content": "@component('mail::message')\n{{ __('You have been invited to join the :team team!', ['team' => $invitation->team->name]) }}\n\n@if (Laravel\\Fortify\\Features::enabled(Laravel\\Fortify\\Features::registration()))\n{{ __('If you do not have an account, you may create one by clicking the button below. After creating an account, you may click the invitation acceptance button in this email to accept the team invitation:') }}\n\n@component('mail::button', ['url' => route('register')])\n{{ __('Create Account') }}\n@endcomponent\n\n{{ __('If you already have an account, you may accept this invitation by clicking the button below:') }}\n\n@else\n{{ __('You may accept this invitation by clicking the button below:') }}\n@endif\n\n\n@component('mail::button', ['url' => $acceptUrl])\n{{ __('Accept Invitation') }}\n@endcomponent\n\n{{ __('If you did not expect to receive an invitation to this team, you may discard this email.') }}\n@endcomponent\n"
  },
  {
    "path": "resources/views/errors/401.blade.php",
    "content": "@extends('errors.layout')\n\n@section('title', __('Unauthorized'))\n@section('code', '401')\n@section('message', __($exception->getMessage() ?: 'Unauthorized'))\n"
  },
  {
    "path": "resources/views/errors/402.blade.php",
    "content": "@extends('errors.minimal')\n\n@section('title', __('Payment Required'))\n@section('code', '402')\n@section('message', __('Payment Required'))\n"
  },
  {
    "path": "resources/views/errors/403.blade.php",
    "content": "@extends('errors.layout')\n\n@section('title', __('Forbidden'))\n@section('code', '403')\n@section('message', __($exception->getMessage() ?: 'You do not have permission to access this resource'))\n"
  },
  {
    "path": "resources/views/errors/404.blade.php",
    "content": "@extends('errors.layout')\n\n@section('title', __('Not Found'))\n@section('code', '404')\n@section('message', __('Not Found'))\n"
  },
  {
    "path": "resources/views/errors/419.blade.php",
    "content": "@extends('errors.layout')\n\n@section('title', __('Page Expired'))\n@section('code', '419')\n@section('message', __('Page Expired'))\n"
  },
  {
    "path": "resources/views/errors/429.blade.php",
    "content": "@extends('errors.layout')\n\n@section('title', __('Too Many Requests'))\n@section('code', '429')\n@section('message', __('Too Many Requests'))\n"
  },
  {
    "path": "resources/views/errors/500.blade.php",
    "content": "@extends('errors.minimal')\n\n@section('title', __('Server Error'))\n@section('code', '500')\n@section('message', __('Something has gone wrong'))\n"
  },
  {
    "path": "resources/views/errors/503.blade.php",
    "content": "@extends('errors.minimal')\n\n@section('title', __('Service Unavailable'))\n@section('code', '503')\n@section('message', __('Vigilant is currently unavailable'))\n"
  },
  {
    "path": "resources/views/errors/layout.blade.php",
    "content": "<x-app-layout>\n    <div class=\"text-center\">\n        <p class=\"font-semibold text-red text-4xl\">@yield('code')</p>\n        <h1 class=\"mt-4 text-3xl font-bold tracking-tight text-white sm:text-5xl\">@yield('title')</h1>\n        <p class=\"mt-6 text-base leading-7 text-base-100\">@yield('message')</p>\n    </div>\n</x-app-layout>\n"
  },
  {
    "path": "resources/views/errors/minimal.blade.php",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" class=\"h-full\">\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    <title>@yield('title')</title>\n\n    <link rel=\"preconnect\" href=\"https://fonts.bunny.net\">\n    <link href=\"https://fonts.googleapis.com/css2?family=Audiowide&display=swap\" rel=\"stylesheet\">\n    <link href=\"https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap\" rel=\"stylesheet\">\n\n    @vite(['resources/css/app.css'])\n</head>\n<body class=\"font-text antialiased dark dark:bg-base-black h-full\">\n<div class=\"fixed h-1.5 z-40 -top-px inset-x-0 bg-gradient-to-r from-green to-red\"></div>\n<main class=\"grid min-h-full place-items-center bg-base-black px-6 py-24 sm:py-32 lg:px-8\">\n    <div class=\"text-center\">\n        <p class=\"text-base font-semibold text-red\">@yield('code')</p>\n        <h1 class=\"mt-4 text-3xl font-header tracking-tight text-white sm:text-5xl\">@yield('title')</h1>\n        <p class=\"mt-6 text-base leading-7 text-base-100\">@yield('message')</p>\n        <div class=\"mt-10 flex items-center justify-center gap-x-6\">\n            <a href=\"/\"\n               class=\"rounded-md bg-red px-3.5 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-red-light focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red\">@lang('Go back')</a>\n        </div>\n    </div>\n</main>\n</body>\n</html>\n"
  },
  {
    "path": "resources/views/layouts/app.blade.php",
    "content": "<!DOCTYPE html>\n<html class=\"h-full\" lang=\"{{ str_replace('_', '-', app()->getLocale()) }}\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"csrf-token\" content=\"{{ csrf_token() }}\">\n\n    <title>@yield('title', '') - Vigilant</title>\n\n    <link rel=\"preconnect\" href=\"https://fonts.bunny.net\">\n    <link href=\"https://fonts.googleapis.com/css2?family=Audiowide&display=swap\" rel=\"stylesheet\">\n    <link href=\"https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap\"\n        rel=\"stylesheet\">\n\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/apple-touch-icon.png\">\n    <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\">\n    <link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\">\n    <link rel=\"manifest\" href=\"/site.webmanifest\">\n\n    @vite(['resources/css/app.css', 'resources/js/app.js'])\n\n    @livewireStyles\n    @stack('head')\n</head>\n\n<body class=\"font-text antialiased dark dark:bg-base-black h-screen flex\" x-data=\"{ sidebarOpen: false, contentShow: true }\"\n    @navigation-start.window=\"contentShow = false\" @navigation-end.window=\"contentShow = true\">\n    <x-layout.sidebar />\n\n    <main class=\"dark:bg-base-black flex flex-col flex-1 relative min-w-0\">\n        <x-layout.topbar />\n        \n        <!-- Diagonal light streaks -->\n        <div class=\"absolute left-0 top-0 w-96 h-full pointer-events-none opacity-30\">\n            <div\n                class=\"absolute top-1/4 -left-24 w-px h-32 bg-gradient-to-b from-transparent via-red to-transparent rotate-12 blur-sm\">\n            </div>\n            <div class=\"absolute top-1/2 -left-32 w-px h-48 bg-gradient-to-b from-transparent via-orange to-transparent rotate-12 blur-sm\"\n                style=\"animation: pulse 3s ease-in-out infinite;\"></div>\n            <div class=\"absolute top-3/4 -left-20 w-px h-40 bg-gradient-to-b from-transparent via-red-light to-transparent rotate-12 blur-sm\"\n                style=\"animation: pulse 3s ease-in-out infinite 1s;\"></div>\n        </div>\n\n        <!-- Floating orbs -->\n        <div\n            class=\"absolute left-4 top-16 w-2 h-2 rounded-full bg-red blur-sm pointer-events-none opacity-60 animate-float\">\n        </div>\n        <div class=\"absolute left-8 top-48 w-1.5 h-1.5 rounded-full bg-orange blur-sm pointer-events-none opacity-40 animate-float\"\n            style=\"animation-delay: -3s;\"></div>\n        <div class=\"absolute left-2 top-96 w-2.5 h-2.5 rounded-full bg-red-light blur-sm pointer-events-none opacity-50 animate-float\"\n            style=\"animation-delay: -1.5s;\"></div>\n\n        <div class=\"bg-base-900 rounded-tl-2xl rounded-tr-2xl lg:rounded-tr-none flex flex-col flex-1 relative overflow-y-auto min-w-0\">\n            @if (isset($header))\n                <header\n                    class=\"bg-gradient-to-r from-base-950 to-base-900 px-4 py-3 sm:px-6 sm:py-4 lg:px-8 border-b border-base-800/50 relative z-10\">\n                    {{ $header }}\n                </header>\n            @endif\n\n            <div class=\"px-4 sm:px-6 lg:px-8 pt-6 pb-6 w-full relative z-10 min-w-0\"\n                x-transition:enter=\"transition ease-out duration-300\" x-transition:enter-start=\"opacity-0\"\n                x-transition:enter-end=\"opacity-100\" x-transition:leave=\"transition ease-in duration-200\"\n                x-transition:leave-start=\"opacity-100\" x-transition:leave-end=\"opacity-0\"\n                :style=\"contentShow ? '' : 'opacity: 0'\">\n                <div>\n                    <x-alert />\n                </div>\n                <x-banner />\n                {{ $slot }}\n            </div>\n\n            <!-- Loading indicator -->\n            <div x-show=\"!contentShow\" x-transition:enter=\"transition ease-out duration-200 delay-100\"\n                x-transition:enter-start=\"opacity-0\" x-transition:enter-end=\"opacity-100\"\n                x-transition:leave=\"transition ease-in duration-150\" x-transition:leave-start=\"opacity-100\"\n                x-transition:leave-end=\"opacity-0\"\n                class=\"absolute inset-0 flex items-center justify-center pointer-events-none z-20\">\n                <div class=\"flex flex-col items-center gap-3\">\n                    <div class=\"relative\">\n                        <div class=\"w-12 h-12 rounded-full border-2 border-base-700\"></div>\n                        <div\n                            class=\"absolute inset-0 w-12 h-12 rounded-full border-2 border-t-red border-r-orange border-b-transparent border-l-transparent animate-spin\">\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </main>\n\n    @stack('modals')\n    @stack('scripts')\n    @livewireScripts\n\n    @if (!ce())\n        <x-dynamic-component component=\"impersonate::banner\" />\n    @endif\n\n</body>\n\n</html>\n"
  },
  {
    "path": "resources/views/layouts/guest.blade.php",
    "content": "<!DOCTYPE html>\n<html lang=\"{{ str_replace('_', '-', app()->getLocale()) }}\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"csrf-token\" content=\"{{ csrf_token() }}\">\n\n    <title>{{ config('app.name', 'Laravel') }}</title>\n\n    <!-- Fonts -->\n    <link rel=\"preconnect\" href=\"https://fonts.bunny.net\">\n    <link href=\"https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap\" rel=\"stylesheet\" />\n\n    <!-- Scripts -->\n    @vite(['resources/css/app.css', 'resources/js/app.js'])\n\n    <!-- Styles -->\n    @livewireStyles\n</head>\n\n<body class=\"font-sans antialiased dark dark:bg-base-black min-w-screen h-screen flex\">\n    <div class=\"fixed h-1.5 z-40 -top-px inset-x-0 bg-gradient-to-r from-green to-red\"></div>\n    <main class=\"dark:bg-base-black flex flex-col overflow-hidden flex-1\">\n        <div\n            class=\"bg-base-900 rounded-tl-2xl rounded-tr-2xl lg:rounded-tr-none overflow-hidden shadow-inner-sm flex flex-col flex-1 pt-px\">\n            <div class=\"px-4 sm:px-6 lg:px-8 pt-6 overflow-y-auto w-full max-h-full\">\n                {{ $slot }}\n            </div>\n        </div>\n    </main>\n    @livewireScripts\n\n    @if (!ce())\n        <x-dynamic-component component=\"impersonate::banner\" />\n    @endif\n</body>\n\n</html>\n"
  },
  {
    "path": "resources/views/policy.blade.php",
    "content": "<x-guest-layout>\n    <div class=\"pt-4 bg-gray-100\">\n        <div class=\"min-h-screen flex flex-col items-center pt-6 sm:pt-0\">\n            <div>\n                <x-authentication-card-logo />\n            </div>\n\n            <div class=\"w-full sm:max-w-2xl mt-6 p-6 bg-white shadow-md overflow-hidden sm:rounded-lg prose\">\n                {!! $policy !!}\n            </div>\n        </div>\n    </div>\n</x-guest-layout>\n"
  },
  {
    "path": "resources/views/profile/delete-user-form.blade.php",
    "content": "<x-action-section>\n    <x-slot name=\"title\">\n        {{ __('Delete Account') }}\n    </x-slot>\n\n    <x-slot name=\"description\">\n        {{ __('Permanently delete your account.') }}\n    </x-slot>\n\n    <x-slot name=\"content\">\n        <div class=\"max-w-xl text-sm text-gray-50\">\n            {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }}\n        </div>\n\n        <div class=\"mt-5\">\n            <x-danger-button wire:click=\"confirmUserDeletion\" wire:loading.attr=\"disabled\">\n                {{ __('Delete Account') }}\n            </x-danger-button>\n        </div>\n\n        <x-dialog-modal wire:model.live=\"confirmingUserDeletion\">\n            <x-slot name=\"title\">\n                {{ __('Delete Account') }}\n            </x-slot>\n\n            <x-slot name=\"content\">\n                {{ __('Are you sure you want to delete your account? Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}\n\n                <div class=\"mt-4\" x-data=\"{}\" x-on:confirming-delete-user.window=\"setTimeout(() => $refs.password.focus(), 250)\">\n                    <x-input type=\"password\" class=\"mt-1 block w-3/4\"\n                                autocomplete=\"current-password\"\n                                placeholder=\"{{ __('Password') }}\"\n                                x-ref=\"password\"\n                                wire:model=\"password\"\n                                wire:keydown.enter=\"deleteUser\" />\n\n                    <x-input-error for=\"password\" class=\"mt-2\" />\n                </div>\n            </x-slot>\n\n            <x-slot name=\"footer\">\n                <x-form.button class=\"bg-base-800\" wire:click=\"$toggle('confirmingUserDeletion')\" wire:loading.attr=\"disabled\">\n                    {{ __('Cancel') }}\n                </x-form.button>\n\n                <x-form.button class=\"ms-3 bg-red\" wire:click=\"deleteUser\" wire:loading.attr=\"disabled\">\n                    {{ __('Delete Account') }}\n                </x-form.button>\n            </x-slot>\n        </x-dialog-modal>\n    </x-slot>\n</x-action-section>\n"
  },
  {
    "path": "resources/views/profile/logout-other-browser-sessions-form.blade.php",
    "content": "<x-action-section>\n    <x-slot name=\"title\">\n        {{ __('Browser Sessions') }}\n    </x-slot>\n\n    <x-slot name=\"description\">\n        {{ __('Manage and log out your active sessions on other browsers and devices.') }}\n    </x-slot>\n\n    <x-slot name=\"content\">\n        <div class=\"max-w-xl text-sm text-gray-50\">\n            {{ __('If necessary, you may log out of all of your other browser sessions across all of your devices. Some of your recent sessions are listed below; however, this list may not be exhaustive. If you feel your account has been compromised, you should also update your password.') }}\n        </div>\n\n        @if (count($this->sessions) > 0)\n            <div class=\"mt-5 space-y-6\">\n                <!-- Other Browser Sessions -->\n                @foreach ($this->sessions as $session)\n                    <div class=\"flex items-center\">\n                        <div>\n                            @if ($session->agent->isDesktop())\n                                <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-8 h-8 text-white\">\n                                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25\" />\n                                </svg>\n                            @else\n                                <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-8 h-8 text-white\">\n                                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3\" />\n                                </svg>\n                            @endif\n                        </div>\n\n                        <div class=\"ms-3\">\n                            <div class=\"text-sm text-gray-50\">\n                                {{ $session->agent->platform() ? $session->agent->platform() : __('Unknown') }} - {{ $session->agent->browser() ? $session->agent->browser() : __('Unknown') }}\n                            </div>\n\n                            <div>\n                                <div class=\"text-xs text-white\">\n                                    {{ $session->ip_address }},\n\n                                    @if ($session->is_current_device)\n                                        <span class=\"text-green-500 font-semibold\">{{ __('This device') }}</span>\n                                    @else\n                                        {{ __('Last active') }} {{ $session->last_active }}\n                                    @endif\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                @endforeach\n            </div>\n        @endif\n\n        <div class=\"flex items-center mt-5\">\n            <x-form.button class=\"bg-red\" wire:click=\"confirmLogout\" wire:loading.attr=\"disabled\">\n                {{ __('Log Out Other Browser Sessions') }}\n            </x-form.button>\n\n            <x-action-message class=\"ms-3 text-green-light\" on=\"loggedOut\">\n                {{ __('Done.') }}\n            </x-action-message>\n        </div>\n\n        <!-- Log Out Other Devices Confirmation Modal -->\n        <x-dialog-modal wire:model.live=\"confirmingLogout\">\n            <x-slot name=\"title\">\n                {{ __('Log Out Other Browser Sessions') }}\n            </x-slot>\n\n            <x-slot name=\"content\">\n                {{ __('Please enter your password to confirm you would like to log out of your other browser sessions across all of your devices.') }}\n\n                <div class=\"mt-4\" x-data=\"{}\" x-on:confirming-logout-other-browser-sessions.window=\"setTimeout(() => $refs.password.focus(), 250)\">\n                    <x-input type=\"password\" class=\"mt-1 block w-3/4\"\n                                autocomplete=\"current-password\"\n                                placeholder=\"{{ __('Password') }}\"\n                                x-ref=\"password\"\n                                wire:model=\"password\"\n                                wire:keydown.enter=\"logoutOtherBrowserSessions\" />\n\n                    <x-input-error for=\"password\" class=\"mt-2\" />\n                </div>\n            </x-slot>\n\n            <x-slot name=\"footer\">\n                <x-form.button class=\"bg-base-800\" wire:click=\"$toggle('confirmingLogout')\" wire:loading.attr=\"disabled\">\n                    {{ __('Cancel') }}\n                </x-form.button>\n\n                <x-form.button class=\"ms-3 bg-red\"\n                            wire:click=\"logoutOtherBrowserSessions\"\n                            wire:loading.attr=\"disabled\">\n                    {{ __('Log Out Other Browser Sessions') }}\n                </x-form.button>\n            </x-slot>\n        </x-dialog-modal>\n    </x-slot>\n</x-action-section>\n"
  },
  {
    "path": "resources/views/profile/two-factor-authentication-form.blade.php",
    "content": "<x-action-section>\n    <x-slot name=\"title\">\n        {{ __('Two Factor Authentication') }}\n    </x-slot>\n\n    <x-slot name=\"description\">\n        {{ __('Add additional security to your account using two factor authentication.') }}\n    </x-slot>\n\n    <x-slot name=\"content\">\n        <h3 class=\"text-lg font-medium text-white\">\n            @if ($this->enabled)\n                @if ($showingConfirmation)\n                    {{ __('Finish enabling two factor authentication.') }}\n                @else\n                    {{ __('You have enabled two factor authentication.') }}\n                @endif\n            @else\n                {{ __('You have not enabled two factor authentication.') }}\n            @endif\n        </h3>\n\n        <div class=\"mt-3 max-w-xl text-sm text-gray-50\">\n            <p>\n                {{ __('When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone\\'s Google Authenticator application.') }}\n            </p>\n        </div>\n\n        @if ($this->enabled)\n            @if ($showingQrCode)\n                <div class=\"mt-4 max-w-xl text-sm text-gray-50\">\n                    <p class=\"font-semibold\">\n                        @if ($showingConfirmation)\n                            {{ __('To finish enabling two factor authentication, scan the following QR code using your phone\\'s authenticator application or enter the setup key and provide the generated OTP code.') }}\n                        @else\n                            {{ __('Two factor authentication is now enabled. Scan the following QR code using your phone\\'s authenticator application or enter the setup key.') }}\n                        @endif\n                    </p>\n                </div>\n\n                <div class=\"mt-4 p-2 inline-block bg-white\">\n                    {!! $this->user->twoFactorQrCodeSvg() !!}\n                </div>\n\n                <div class=\"mt-4 max-w-xl text-sm text-gray-50\">\n                    <p class=\"font-semibold\">\n                        {{ __('Setup Key') }}: {{ decrypt($this->user->two_factor_secret) }}\n                    </p>\n                </div>\n\n                @if ($showingConfirmation)\n                    <div class=\"mt-4\">\n                        <x-label for=\"code\" value=\"{{ __('Code') }}\" />\n\n                        <x-input id=\"code\" type=\"text\" name=\"code\" class=\"block mt-1 w-1/2\" inputmode=\"numeric\" autofocus autocomplete=\"one-time-code\"\n                            wire:model=\"code\"\n                            wire:keydown.enter=\"confirmTwoFactorAuthentication\" />\n\n                        <x-input-error for=\"code\" class=\"mt-2\" />\n                    </div>\n                @endif\n            @endif\n\n            @if ($showingRecoveryCodes)\n                <div class=\"mt-4 max-w-xl text-sm text-gray-50\">\n                    <p class=\"font-semibold\">\n                        {{ __('Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.') }}\n                    </p>\n                </div>\n\n                <div class=\"grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg\">\n                    @foreach (json_decode(decrypt($this->user->two_factor_recovery_codes), true) as $code)\n                        <div>{{ $code }}</div>\n                    @endforeach\n                </div>\n            @endif\n        @endif\n\n        <div class=\"mt-5\">\n            @if (! $this->enabled)\n                <x-confirms-password wire:then=\"enableTwoFactorAuthentication\">\n                    <x-form.button class=\"bg-green\" type=\"button\" wire:loading.attr=\"disabled\">\n                        {{ __('Enable') }}\n                    </x-form.button>\n                </x-confirms-password>\n            @else\n                @if ($showingRecoveryCodes)\n                    <x-confirms-password wire:then=\"regenerateRecoveryCodes\">\n                        <x-form.button class=\"me-3 bg-base-800\">\n                            {{ __('Regenerate Recovery Codes') }}\n                        </x-form.button>\n                    </x-confirms-password>\n                @elseif ($showingConfirmation)\n                    <x-confirms-password wire:then=\"confirmTwoFactorAuthentication\">\n                        <x-form.button type=\"button\" class=\"me-3 bg-green\" wire:loading.attr=\"disabled\">\n                            {{ __('Confirm') }}\n                        </x-form.button>\n                    </x-confirms-password>\n                @else\n                    <x-confirms-password wire:then=\"showRecoveryCodes\">\n                        <x-form.button class=\"me-3 bg-base-800\">\n                            {{ __('Show Recovery Codes') }}\n                        </x-form.button>\n                    </x-confirms-password>\n                @endif\n\n                @if ($showingConfirmation)\n                    <x-confirms-password wire:then=\"disableTwoFactorAuthentication\">\n                        <x-form.button class=\"bg-base-800\" wire:loading.attr=\"disabled\">\n                            {{ __('Cancel') }}\n                        </x-form.button>\n                    </x-confirms-password>\n                @else\n                    <x-confirms-password wire:then=\"disableTwoFactorAuthentication\">\n                        <x-form.button class=\"bg-red\" wire:loading.attr=\"disabled\">\n                            {{ __('Disable') }}\n                        </x-form.button>\n                    </x-confirms-password>\n                @endif\n\n            @endif\n        </div>\n    </x-slot>\n</x-action-section>\n"
  },
  {
    "path": "resources/views/profile/update-password-form.blade.php",
    "content": "<x-form-section submit=\"updatePassword\">\n    <x-slot name=\"title\">\n        {{ __('Update Password') }}\n    </x-slot>\n\n    <x-slot name=\"description\">\n        {{ __('Ensure your account is using a long, random password to stay secure.') }}\n    </x-slot>\n\n    <x-slot name=\"form\">\n        <div class=\"col-span-6 sm:col-span-4\">\n            <x-label for=\"current_password\" value=\"{{ __('Current Password') }}\" />\n            <x-input id=\"current_password\" type=\"password\" class=\"mt-1 block w-full\" wire:model=\"state.current_password\" autocomplete=\"current-password\" />\n            <x-input-error for=\"current_password\" class=\"mt-2\" />\n        </div>\n\n        <div class=\"col-span-6 sm:col-span-4\">\n            <x-label for=\"password\" value=\"{{ __('New Password') }}\" />\n            <x-input id=\"password\" type=\"password\" class=\"mt-1 block w-full\" wire:model=\"state.password\" autocomplete=\"new-password\" />\n            <x-input-error for=\"password\" class=\"mt-2\" />\n        </div>\n\n        <div class=\"col-span-6 sm:col-span-4\">\n            <x-label for=\"password_confirmation\" value=\"{{ __('Confirm Password') }}\" />\n            <x-input id=\"password_confirmation\" type=\"password\" class=\"mt-1 block w-full\" wire:model=\"state.password_confirmation\" autocomplete=\"new-password\" />\n            <x-input-error for=\"password_confirmation\" class=\"mt-2\" />\n        </div>\n    </x-slot>\n\n    <x-slot name=\"actions\">\n        <x-action-message class=\"me-3 text-green-light\" on=\"saved\">\n            {{ __('Saved.') }}\n        </x-action-message>\n\n        <x-form.button class=\"bg-red\">\n            {{ __('Save') }}\n        </x-form.button>\n    </x-slot>\n</x-form-section>\n"
  },
  {
    "path": "resources/views/profile/update-profile-information-form.blade.php",
    "content": "<div>\n    <form wire:submit=\"updateProfileInformation\">\n\n        @if (Laravel\\Jetstream\\Jetstream::managesProfilePhotos())\n            <div x-data=\"{photoName: null, photoPreview: null}\" class=\"col-span-6 sm:col-span-4\">\n                <!-- Profile Photo File Input -->\n                <input type=\"file\" id=\"photo\" class=\"hidden\"\n                       wire:model.live=\"photo\"\n                       x-ref=\"photo\"\n                       x-on:change=\"\n                                    photoName = $refs.photo.files[0].name;\n                                    const reader = new FileReader();\n                                    reader.onload = (e) => {\n                                        photoPreview = e.target.result;\n                                    };\n                                    reader.readAsDataURL($refs.photo.files[0]);\n                            \"/>\n\n                <x-label for=\"photo\" value=\"{{ __('Photo') }}\"/>\n\n                <!-- Current Profile Photo -->\n                <div class=\"mt-2\" x-show=\"! photoPreview\">\n                    <img src=\"{{ $this->user->profile_photo_url }}\" alt=\"{{ $this->user->name }}\"\n                         class=\"rounded-full h-20 w-20 object-cover\">\n                </div>\n\n                <!-- New Profile Photo Preview -->\n                <div class=\"mt-2\" x-show=\"photoPreview\" style=\"display: none;\">\n                    <span class=\"block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center\"\n                          x-bind:style=\"'background-image: url(\\'' + photoPreview + '\\');'\">\n                    </span>\n                </div>\n\n                <x-secondary-button class=\"mt-2 me-2\" type=\"button\" x-on:click.prevent=\"$refs.photo.click()\">\n                    {{ __('Select A New Photo') }}\n                </x-secondary-button>\n\n                @if ($this->user->profile_photo_path)\n                    <x-secondary-button type=\"button\" class=\"mt-2\" wire:click=\"deleteProfilePhoto\">\n                        {{ __('Remove Photo') }}\n                    </x-secondary-button>\n                @endif\n\n                <x-input-error for=\"photo\" class=\"mt-2\"/>\n            </div>\n        @endif\n\n        <x-form.text class=\"sm:col-span-2\"\n                     field=\"state.name\"\n                     name=\"Name\"\n                     description=\"Name\"\n                     required\n                     autocomplete=\"name\"\n                     placeholder=\"{{ config('app.url') }}\"/>\n\n        {{--    <div class=\"col-span-6 sm:col-span-4\">--}}\n        {{--        <x-label for=\"name\" value=\"{{ __('Name') }}\"/>--}}\n        {{--        <x-input id=\"name\" type=\"text\" class=\"mt-1 block w-full\" wire:model=\"state.name\" required autocomplete=\"name\"/>--}}\n        {{--        <x-input-error for=\"name\" class=\"mt-2\"/>--}}\n        {{--    </div>--}}\n\n        <div class=\"col-span-6 sm:col-span-4\">\n            <x-label for=\"email\" value=\"{{ __('Email') }}\"/>\n            <x-input id=\"email\" type=\"email\" class=\"mt-1 block w-full\" wire:model=\"state.email\" required\n                     autocomplete=\"username\"/>\n            <x-input-error for=\"email\" class=\"mt-2\"/>\n\n            @if (Laravel\\Fortify\\Features::enabled(Laravel\\Fortify\\Features::emailVerification()) && ! $this->user->hasVerifiedEmail())\n                <p class=\"text-sm mt-2\">\n                    {{ __('Your email address is unverified.') }}\n\n                    <button type=\"button\"\n                            class=\"underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red\"\n                            wire:click.prevent=\"sendEmailVerification\">\n                        {{ __('Click here to re-send the verification email.') }}\n                    </button>\n                </p>\n\n                @if ($this->verificationLinkSent)\n                    <p class=\"mt-2 font-medium text-sm text-green-600\">\n                        {{ __('A new verification link has been sent to your email address.') }}\n                    </p>\n                @endif\n            @endif\n        </div>\n\n        <x-action-message class=\"me-3 text-green-light\" on=\"saved\">\n            {{ __('Saved.') }}\n        </x-action-message>\n\n        <x-form.submit-button class=\"bg-red\" wire:loading.attr=\"disabled\" wire:target=\"photo\" submitText=\"Save\"/>\n    </form>\n\n</div>\n"
  },
  {
    "path": "resources/views/teams/create-team-form.blade.php",
    "content": "<x-form-section submit=\"createTeam\">\n    <x-slot name=\"title\">\n        {{ __('Team Details') }}\n    </x-slot>\n\n    <x-slot name=\"description\">\n        {{ __('Create a new team to separate Vigilant\\'s components') }}\n    </x-slot>\n\n    <x-slot name=\"form\">\n        <div class=\"col-span-6\">\n            <x-label value=\"{{ __('Team Owner') }}\" />\n\n            <div class=\"flex items-center mt-2\">\n                <div class=\"leading-tight\">\n                    <div class=\"text-base-100\">{{ $this->user->name }}</div>\n                    <div class=\"text-base-100 text-sm\">{{ $this->user->email }}</div>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"col-span-6 sm:col-span-4\">\n            <x-label for=\"name\" value=\"{{ __('Team Name') }}\" class=\"mb-2\" />\n            <x-input id=\"name\" type=\"text\" class=\"mt-1 block w-full\" wire:model=\"state.name\" autofocus />\n            <x-input-error for=\"name\" class=\"mt-2\" />\n        </div>\n    </x-slot>\n\n    <x-slot name=\"actions\">\n        <x-form.button class=\"bg-red\">\n            {{ __('Create') }}\n        </x-form.button>\n    </x-slot>\n</x-form-section>\n"
  },
  {
    "path": "resources/views/teams/create.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header title=\"Create Team\"></x-page-header>\n    </x-slot>\n\n    <div>\n        <div class=\"max-w-7xl mx-auto py-10 sm:px-6 lg:px-8\">\n            @livewire('teams.create-team-form')\n        </div>\n    </div>\n</x-app-layout>\n"
  },
  {
    "path": "resources/views/teams/delete-team-form.blade.php",
    "content": "<x-action-section>\n    <x-slot name=\"title\">\n        {{ __('Delete Team') }}\n    </x-slot>\n\n    <x-slot name=\"description\">\n        {{ __('Permanently delete this team.') }}\n    </x-slot>\n\n    <x-slot name=\"content\">\n        <div class=\"max-w-xl text-sm text-base-100\">\n            {{ __('Once a team is deleted, all of its resources and data will be permanently deleted. Before deleting this team, please download any data or information regarding this team that you wish to retain.') }}\n            <br/>\n            <span class=\"text-red text-md\">{{ __('This action is non-reverable, all data will be lost.') }}</span>\n        </div>\n\n        <div class=\"mt-5\">\n\n            <x-danger-button wire:click=\"$toggle('confirmingTeamDeletion')\" wire:loading.attr=\"disabled\">\n                {{ __('Delete Team') }}\n            </x-danger-button>\n        </div>\n\n        <x-confirmation-modal wire:model.live=\"confirmingTeamDeletion\">\n            <x-slot name=\"title\">\n                {{ __('Delete Team') }}\n            </x-slot>\n\n            <x-slot name=\"content\">\n                {{ __('Are you sure you want to delete this team? Once a team is deleted, all of its resources and data will be permanently deleted.') }}\n            </x-slot>\n\n            <x-slot name=\"footer\">\n                <x-button wire:click=\"$toggle('confirmingTeamDeletion')\" wire:loading.attr=\"disabled\">\n                    {{ __('Cancel') }}\n                </x-button>\n\n                <x-danger-button class=\"ms-3\" wire:click=\"deleteTeam\" wire:loading.attr=\"disabled\">\n                    {{ __('Delete Team') }}\n                </x-danger-button>\n            </x-slot>\n        </x-confirmation-modal>\n    </x-slot>\n</x-action-section>\n"
  },
  {
    "path": "resources/views/teams/show.blade.php",
    "content": "<x-app-layout>\n    <x-slot name=\"header\">\n        <x-page-header title=\"Team Settings\"/>\n    </x-slot>\n\n    <div>\n        <div class=\"max-w-7xl mx-auto py-10 sm:px-6 lg:px-8\">\n            @livewire('teams.update-team-name-form', ['team' => $team])\n\n            @livewire('teams.team-member-manager', ['team' => $team])\n\n            @if (Gate::check('delete', $team) && ! $team->personal_team)\n                <x-section-border />\n\n                <div class=\"mt-10 sm:mt-0\">\n                    @livewire('teams.delete-team-form', ['team' => $team])\n                </div>\n            @endif\n        </div>\n    </div>\n</x-app-layout>\n"
  },
  {
    "path": "resources/views/teams/team-member-manager.blade.php",
    "content": "<div>\n    @if (Gate::check('addTeamMember', $team))\n        <x-section-border />\n\n        <div class=\"mt-10 sm:mt-0\">\n            <x-form-section submit=\"addTeamMember\">\n                <x-slot name=\"title\">\n                    {{ __('Add Team Member') }}\n                </x-slot>\n\n                <x-slot name=\"description\">\n                    {{ __('Add a new team member to your team, allowing them to collaborate with you.') }}\n                </x-slot>\n\n                <x-slot name=\"form\">\n                    <div class=\"col-span-6\">\n                        <div class=\"max-w-xl text-sm text-base-100\">\n                            {{ __('Please provide the email address of the person you would like to add to this team.') }}\n                        </div>\n                    </div>\n\n                    <div class=\"col-span-6 sm:col-span-4\">\n                        <x-label for=\"email\" value=\"{{ __('Email') }}\" />\n                        <x-input id=\"email\" type=\"email\" class=\"mt-1 block w-full\" wire:model=\"addTeamMemberForm.email\" />\n                        <x-input-error for=\"email\" class=\"mt-2\" />\n                    </div>\n\n                    @if (count($this->roles) > 0)\n                        <div class=\"col-span-6 lg:col-span-4\">\n                            <x-label for=\"role\" value=\"{{ __('Role') }}\" />\n                            <x-input-error for=\"role\" class=\"mt-2\" />\n\n                            <div class=\"relative z-0 mt-1 border border-gray-200 rounded-lg cursor-pointer\">\n                                @foreach ($this->roles as $index => $role)\n                                    <button type=\"button\" class=\"relative px-4 py-3 inline-flex w-full rounded-lg focus:z-10 focus:outline-hidden focus:border-red focus:ring-2 focus:ring-red {{ $index > 0 ? 'border-t border-gray-200 focus:border-none rounded-t-none' : '' }} {{ ! $loop->last ? 'rounded-b-none' : '' }}\"\n                                                    wire:click=\"$set('addTeamMemberForm.role', '{{ $role->key }}')\">\n                                        <div class=\"{{ isset($addTeamMemberForm['role']) && $addTeamMemberForm['role'] !== $role->key ? 'opacity-50' : '' }}\">\n                                            <div class=\"flex items-center\">\n                                                <div class=\"text-sm text-base-100 {{ $addTeamMemberForm['role'] == $role->key ? 'font-semibold' : '' }}\">\n                                                    {{ $role->name }}\n                                                </div>\n\n                                                @if ($addTeamMemberForm['role'] == $role->key)\n                                                    <svg class=\"ms-2 h-5 w-5 text-green-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n                                                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n                                                    </svg>\n                                                @endif\n                                            </div>\n\n                                            <div class=\"mt-2 text-xs text-white text-start\">\n                                                {{ $role->description }}\n                                            </div>\n                                        </div>\n                                    </button>\n                                @endforeach\n                            </div>\n                        </div>\n                    @endif\n                </x-slot>\n\n                <x-slot name=\"actions\">\n                    <x-action-message class=\"me-3\" on=\"saved\">\n                        {{ __('Added.') }}\n                    </x-action-message>\n\n                    <x-form.button class=\"bg-red\">\n                        {{ __('Add') }}\n                    </x-form.button>\n                </x-slot>\n            </x-form-section>\n        </div>\n    @endif\n\n    @if ($team->teamInvitations->isNotEmpty() && Gate::check('addTeamMember', $team))\n        <x-section-border />\n\n        <div class=\"mt-10 sm:mt-0\">\n            <x-action-section>\n                <x-slot name=\"title\">\n                    {{ __('Pending Team Invitations') }}\n                </x-slot>\n\n                <x-slot name=\"description\">\n                    {{ __('These people have been invited to your team and have been sent an invitation email. They may join the team by accepting the email invitation.') }}\n                </x-slot>\n\n                <x-slot name=\"content\">\n                    <div class=\"space-y-6\">\n                        @foreach ($team->teamInvitations as $invitation)\n                            <div class=\"flex items-center justify-between\">\n                                <div class=\"text-base-100\">{{ $invitation->email }}</div>\n\n                                <div class=\"flex items-center\">\n                                    @if (Gate::check('removeTeamMember', $team))\n                                        <button class=\"cursor-pointer ms-6 text-sm text-red-500 focus:outline-hidden\"\n                                                            wire:click=\"cancelTeamInvitation({{ $invitation->id }})\">\n                                            {{ __('Cancel') }}\n                                        </button>\n                                    @endif\n                                </div>\n                            </div>\n                        @endforeach\n                    </div>\n                </x-slot>\n            </x-action-section>\n        </div>\n    @endif\n\n    @if ($team->users->isNotEmpty())\n        <x-section-border />\n\n        <!-- Manage Team Members -->\n        <div class=\"mt-10 sm:mt-0\">\n            <x-action-section>\n                <x-slot name=\"title\">\n                    {{ __('Team Members') }}\n                </x-slot>\n\n                <x-slot name=\"description\">\n                    {{ __('All of the people that are part of this team.') }}\n                </x-slot>\n\n                <x-slot name=\"content\">\n                    <div class=\"space-y-6\">\n                        @foreach ($team->users->sortBy('name') as $user)\n                            <div class=\"flex items-center justify-between\">\n                                <div class=\"flex items-center\">\n                                    <img class=\"w-8 h-8 rounded-full object-cover\" src=\"{{ $user->profile_photo_url }}\" alt=\"{{ $user->name }}\">\n                                    <div class=\"ms-4 text-white\">{{ $user->name }}</div>\n                                </div>\n\n                                <div class=\"flex items-center\">\n                                    @if (Gate::check('updateTeamMember', $team) && Laravel\\Jetstream\\Jetstream::hasRoles())\n                                        <button class=\"ms-2 text-sm text-gray-400 underline\" wire:click=\"manageRole('{{ $user->id }}')\">\n                                            {{ Laravel\\Jetstream\\Jetstream::findRole($user->membership->role)->name }}\n                                        </button>\n                                    @elseif (Laravel\\Jetstream\\Jetstream::hasRoles())\n                                        <div class=\"ms-2 text-sm text-gray-400\">\n                                            {{ Laravel\\Jetstream\\Jetstream::findRole($user->membership->role)->name }}\n                                        </div>\n                                    @endif\n\n                                    @if ($this->user->id === $user->id)\n                                        <button class=\"cursor-pointer ms-6 text-sm text-red-500\" wire:click=\"$toggle('confirmingLeavingTeam')\">\n                                            {{ __('Leave') }}\n                                        </button>\n\n                                    @elseif (Gate::check('removeTeamMember', $team))\n                                        <button class=\"cursor-pointer ms-6 text-sm text-red-500\" wire:click=\"confirmTeamMemberRemoval('{{ $user->id }}')\">\n                                            {{ __('Remove') }}\n                                        </button>\n                                    @endif\n                                </div>\n                            </div>\n                        @endforeach\n                    </div>\n                </x-slot>\n            </x-action-section>\n        </div>\n    @endif\n\n    <x-dialog-modal wire:model.live=\"currentlyManagingRole\">\n        <x-slot name=\"title\">\n            {{ __('Manage Role') }}\n        </x-slot>\n\n        <x-slot name=\"content\">\n            <div class=\"relative z-0 mt-1 border border-gray-200 rounded-lg cursor-pointer\">\n                @foreach ($this->roles as $index => $role)\n                    <button type=\"button\" class=\"relative px-4 py-3 inline-flex w-full rounded-lg focus:z-10 focus:outline-hidden focus:border-red focus:ring-2 focus:ring-red {{ $index > 0 ? 'border-t border-gray-200 focus:border-none rounded-t-none' : '' }} {{ ! $loop->last ? 'rounded-b-none' : '' }}\"\n                                    wire:click=\"$set('currentRole', '{{ $role->key }}')\">\n                        <div class=\"{{ $currentRole !== $role->key ? 'opacity-50' : '' }}\">\n                            <div class=\"flex items-center\">\n                                <div class=\"text-sm text-gray-600 {{ $currentRole == $role->key ? 'font-semibold' : '' }}\">\n                                    {{ $role->name }}\n                                </div>\n\n                                @if ($currentRole == $role->key)\n                                    <svg class=\"ms-2 h-5 w-5 text-green-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n                                        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n                                    </svg>\n                                @endif\n                            </div>\n\n                            <div class=\"mt-2 text-xs text-gray-600\">\n                                {{ $role->description }}\n                            </div>\n                        </div>\n                    </button>\n                @endforeach\n            </div>\n        </x-slot>\n\n        <x-slot name=\"footer\">\n            <x-secondary-button wire:click=\"stopManagingRole\" wire:loading.attr=\"disabled\">\n                {{ __('Cancel') }}\n            </x-secondary-button>\n\n            <x-button class=\"ms-3\" wire:click=\"updateRole\" wire:loading.attr=\"disabled\">\n                {{ __('Save') }}\n            </x-button>\n        </x-slot>\n    </x-dialog-modal>\n\n    <!-- Leave Team Confirmation Modal -->\n    <x-confirmation-modal wire:model.live=\"confirmingLeavingTeam\">\n        <x-slot name=\"title\">\n            {{ __('Leave Team') }}\n        </x-slot>\n\n        <x-slot name=\"content\">\n            {{ __('Are you sure you would like to leave this team?') }}\n        </x-slot>\n\n        <x-slot name=\"footer\">\n            <x-secondary-button wire:click=\"$toggle('confirmingLeavingTeam')\" wire:loading.attr=\"disabled\">\n                {{ __('Cancel') }}\n            </x-secondary-button>\n\n            <x-danger-button class=\"ms-3\" wire:click=\"leaveTeam\" wire:loading.attr=\"disabled\">\n                {{ __('Leave') }}\n            </x-danger-button>\n        </x-slot>\n    </x-confirmation-modal>\n\n    <!-- Remove Team Member Confirmation Modal -->\n    <x-confirmation-modal wire:model.live=\"confirmingTeamMemberRemoval\">\n        <x-slot name=\"title\">\n            {{ __('Remove Team Member') }}\n        </x-slot>\n\n        <x-slot name=\"content\">\n            {{ __('Are you sure you would like to remove this person from the team?') }}\n        </x-slot>\n\n        <x-slot name=\"footer\">\n            <x-secondary-button wire:click=\"$toggle('confirmingTeamMemberRemoval')\" wire:loading.attr=\"disabled\">\n                {{ __('Cancel') }}\n            </x-secondary-button>\n\n            <x-danger-button class=\"ms-3\" wire:click=\"removeTeamMember\" wire:loading.attr=\"disabled\">\n                {{ __('Remove') }}\n            </x-danger-button>\n        </x-slot>\n    </x-confirmation-modal>\n</div>\n"
  },
  {
    "path": "resources/views/teams/update-team-name-form.blade.php",
    "content": "<x-form-section submit=\"updateTeamName\">\n    <x-slot name=\"title\">\n        {{ __('Team Information') }}\n    </x-slot>\n\n    <x-slot name=\"description\">\n        {{ __('Update the team name and timezone') }}\n    </x-slot>\n\n    <x-slot name=\"form\">\n        <div class=\"col-span-6\">\n            <x-label value=\"{{ __('Team Owner') }}\"/>\n\n            <div class=\"flex items-center mt-2\">\n                <div class=\"leading-tight\">\n                    <div class=\"text-base-100\">{{ $team->owner->name }}</div>\n                    <div class=\"text-white text-sm\">{{ $team->owner->email }}</div>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"col-span-6 sm:col-span-4\">\n            <x-label for=\"name\" value=\"{{ __('Team Name') }}\"/>\n\n            <x-input id=\"name\"\n                     type=\"text\"\n                     class=\"mt-1 block w-full\"\n                     wire:model=\"state.name\"\n                     :disabled=\"! Gate::check('update', $team)\"/>\n\n            <x-label class=\"mt-2\" for=\"state.timezone\" value=\"{{ __('Timezone') }}\"/>\n\n            <x-form.select field=\"state.timezone\"\n                           :inline=\"true\"\n            >\n                @foreach(DateTimeZone::listIdentifiers() as $timezone)\n                    <option value=\"{{ $timezone }}\">{{ $timezone }}</option>\n                @endforeach\n            </x-form.select>\n\n            <x-input-error for=\"name\" class=\"mt-2\"/>\n        </div>\n    </x-slot>\n\n    @if (Gate::check('update', $team))\n        <x-slot name=\"actions\">\n            <x-action-message class=\"me-3 text-green\" on=\"saved\">\n                {{ __('Saved.') }}\n            </x-action-message>\n\n            <x-form.button class=\"bg-red\">\n                {{ __('Save') }}\n            </x-form.button>\n        </x-slot>\n    @endif\n</x-form-section>\n"
  },
  {
    "path": "resources/views/terms.blade.php",
    "content": "<x-guest-layout>\n    <div class=\"pt-4 bg-gray-100\">\n        <div class=\"min-h-screen flex flex-col items-center pt-6 sm:pt-0\">\n            <div>\n                <x-authentication-card-logo />\n            </div>\n\n            <div class=\"w-full sm:max-w-2xl mt-6 p-6 bg-white shadow-md overflow-hidden sm:rounded-lg prose\">\n                {!! $terms !!}\n            </div>\n        </div>\n    </div>\n</x-guest-layout>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/columns/buttons/copy.blade.php",
    "content": "<x-livewire-table::button\n    size=\"sm\"\n    :title=\"__('Copy')\"\n    :aria-label=\"__('Copy')\"\n    class=\"!absolute top-1 right-1 z-10 opacity-0 group-hover/column:opacity-100 cursor-pointer\"\n    x-on:click.stop=\"copy\"\n    x-data=\"{\n        copied: false,\n        copy: async function () {\n            try {\n                const text = $refs.content.innerText;\n\n                await navigator.clipboard.writeText(text);\n\n                this.copied = true;\n\n                setTimeout(() => this.copied = false, 1000);\n            } catch (error) {\n                console.error(error.message);\n            }\n        }\n    }\"\n>\n    <template x-if=\"! copied\">\n        <x-livewire-table::icon class=\"size-5\" icon=\"clipboard-document\" />\n    </template>\n    <template x-if=\"copied\">\n        <x-livewire-table::icon class=\"size-5\" icon=\"clipboard-document-check\" />\n    </template>\n</x-livewire-table::button>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/columns/content/action.blade.php",
    "content": "@php($actions = $this->resolveActions()->standalone(false)->canBeRun($model))\n\n<div class=\"px-3 py-1\">\n    <x-livewire-table::dropdown current=\"actions\">\n        <x-livewire-table::button\n            size=\"sm\"\n            :title=\"__('Actions')\"\n            :aria-label=\"__('Actions')\"\n            x-on:click=\"toggle\"\n        >\n            <x-livewire-table::icon class=\"size-5\" icon=\"play\" />\n        </x-livewire-table::button>\n        <x-slot:body>\n            <x-livewire-table::dropdown.section section=\"actions\">\n                <x-livewire-table::dropdown.header :label=\"__('Actions')\" icon=\"play\" />\n                <x-livewire-table::dropdown.content>\n                    <x-livewire-table::dropdown.menu x-data=\"{ selected: [item] }\">\n                        @foreach($actions as $action)\n                            @if($action->isScript())\n                                <x-livewire-table::dropdown.menu.item\n                                    :label=\"$action->label()\"\n                                    wire:key=\"{{ $action->code() }}\"\n                                    x-bind:disabled=\"selected.length === 0\"\n                                    x-on:click=\"\n                                        {{ $action->script() }}\n                                        close()\n                                    \"\n                                />\n                            @else\n                                <x-livewire-table::dropdown.menu.item\n                                    :label=\"$action->label()\"\n                                    wire:key=\"{{ $action->code() }}\"\n                                    x-bind:disabled=\"selected.length === 0\"\n                                    wire:click=\"executeItemAction({{ Js::from($action->code()) }}, item)\"\n                                    x-on:click=\"close\"\n                                />\n                            @endif\n                        @endforeach\n                    </x-livewire-table::dropdown.menu>\n                </x-livewire-table::dropdown.content>\n            </x-livewire-table::dropdown.section>\n        </x-slot:body>\n    </x-livewire-table::dropdown>\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/columns/content/boolean.blade.php",
    "content": "<div class=\"px-4 py-3\">\n    @if($value === null)\n        <div class=\"mx-auto size-3 border-2 border-base-500 rounded-full\"></div>\n    @elseif($value)\n        <div class=\"mx-auto size-3 border-2 border-green-500 rounded-full\"></div>\n    @else\n        <div class=\"mx-auto size-3 border-2 border-red rounded-full\"></div>\n    @endif\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/columns/content/default.blade.php",
    "content": "<div class=\"px-3 py-2 text-black dark:text-white transition\">\n    @if($value === null)\n        <span class=\"opacity-25\">&mdash;</span>\n    @elseif($column->isRaw())\n        {!! $value !!}\n    @else\n        {{ $value }}\n    @endif\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/columns/content/image.blade.php",
    "content": "<div class=\"px-3 py-1\">\n    @if($value)\n        <img\n            class=\"max-w-none\"\n            src=\"{{ $value }}\"\n            alt=\"{{ $column->label() }}\"\n            width=\"{{ $column->getWidth() }}\"\n            height=\"{{ $column->getHeight() }}\"\n        >\n    @endif\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/columns/footer/default.blade.php",
    "content": "<div class=\"px-3 py-2 truncate\">\n    @if(($content = $column->getFooterContent()) !== null)\n        {!! $content !!}\n    @else\n        &nbsp;\n    @endif\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/columns/header/default.blade.php",
    "content": "@if($column->isSortable())\n    <button\n       type=\"button\"\n       wire:click=\"sort(@js($column->code()))\"\n       class=\"flex items-center w-full gap-3 px-4 py-1 whitespace-nowrap cursor-pointer text-left text-base-100 hover:text-base-50 hover:bg-base-800/50 transition-all duration-200 rounded-lg group\"\n    >\n        <span class=\"flex-1 font-semibold\">{{ $column->label() }}</span>\n        @if(! $this->isReordering())\n            @if($this->sortColumn === $column->code())\n                @if($this->sortDirection === 'asc')\n                    <x-livewire-table::icon icon=\"chevron-up\" class=\"size-4 text-red flex-shrink-0\" />\n                @else\n                    <x-livewire-table::icon icon=\"chevron-down\" class=\"size-4 text-red flex-shrink-0\" />\n                @endif\n            @else\n                <x-livewire-table::icon icon=\"chevron-up-down\" class=\"size-4 opacity-50 group-hover:opacity-100 transition-opacity duration-200 flex-shrink-0\" />\n            @endif\n        @endif\n    </button>\n@else\n    <span class=\"block px-4 py-1 whitespace-nowrap font-semibold text-base-100\">{{ $column->label() }}</span>\n@endif\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/columns/search/boolean.blade.php",
    "content": "<div class=\"px-3 pb-2\">\n    <x-livewire-table::form.select\n        wire:model.live=\"search.{{ $column->code() }}\"\n        size=\"sm\"\n    >\n        <option value=\"\">&mdash;</option>\n        <option value=\"1\">@lang('Yes')</option>\n        <option value=\"0\">@lang('No')</option>\n    </x-livewire-table::form.select>\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/columns/search/date.blade.php",
    "content": "<div class=\"px-3 pb-2\">\n    <x-livewire-table::form.input\n        wire:model.live.debounce.500ms=\"search.{{ $column->code() }}\"\n        size=\"sm\"\n        type=\"date\"\n    />\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/columns/search/default.blade.php",
    "content": "<div class=\"px-3\">\n    <x-livewire-table::form.input\n        wire:model.live.debounce.500ms=\"search.{{ $column->code() }}\"\n        size=\"sm\"\n        type=\"search\"\n        :placeholder=\"__('Search...')\"\n    />\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/columns/search/select.blade.php",
    "content": "<div class=\"px-3 pb-2\">\n    <x-livewire-table::form.select\n        wire:model.live=\"search.{{ $column->code() }}\"\n        size=\"sm\"\n    >\n        <option value=\"\">&mdash;</option>\n        @foreach($column->getOptions() as $key => $value)\n            @if(is_array($value))\n                <optgroup wire:key=\"{{ $key }}\" label=\"{{ $key }}\">\n                    @foreach($value as $key2 => $value2)\n                        <option wire:key=\"{{ $key2 }}\" value=\"{{ $key2 }}\">{{ $value2 }}</option>\n                    @endforeach\n                </optgroup>\n            @else\n                <option wire:key=\"{{ $key }}\" value=\"{{ $key }}\">{{ $value }}</option>\n            @endif\n        @endforeach\n    </x-livewire-table::form.select>\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/button.blade.php",
    "content": "@props(['size' => 'md', 'active' => false, 'dot' => false])\n\n<button\n    {{\n        $attributes->merge([\n            'type' => 'button',\n        ])->class([\n            'relative flex items-center rounded-lg border cursor-pointer transition-all duration-200',\n            'focus:outline-none focus:ring-2 focus:ring-red/50 focus:z-10',\n            'bg-base-850 hover:bg-base-800 active:bg-base-800',\n            'border-base-700 hover:border-base-600 focus:border-red',\n            'text-base-100 hover:text-base-50 active:text-base-50' => ! $active,\n            'text-red border-red bg-red/10' => $active,\n            'px-3 py-2' => $size === 'md',\n            'px-2 py-1' => $size === 'sm',\n        ])\n    }}\n>\n    {{ $slot }}\n    @if($dot)\n        <span class=\"absolute right-2 top-1 rounded-full shadow-xs bg-red block size-2\"></span>\n    @endif\n</button>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/dropdown/content.blade.php",
    "content": "<x-livewire-table::dropdown.divider class=\"overflow-y-auto max-h-96\">\n    {{ $slot }}\n</x-livewire-table::dropdown.divider>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/dropdown/divider.blade.php",
    "content": "<div {{ $attributes->class('divide-y-1 divide-solid divide-base-700') }}>\n    {{ $slot }}\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/dropdown/footer.blade.php",
    "content": "<p {{ $attributes->class('px-4 py-2 text-sm transition-all duration-200 text-base-400') }}>\n    {{ $slot }}\n</p>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/dropdown/header.blade.php",
    "content": "@props(['label', 'icon', 'navigate' => null])\n\n<header class=\"flex items-center border-b border-base-700 transition-all duration-200\">\n    <button\n        {{\n            $attributes->merge([\n                'type' => 'button',\n            ])->class([\n                'flex items-center gap-3 w-full text-sm px-4 py-3 border transition-all duration-200',\n                'bg-base-900',\n                'border-base-900',\n                'text-base-100',\n                'cursor-pointer' => $navigate,\n                'focus:outline-none focus:ring-2 focus:ring-red/50 focus:z-10' => $navigate,\n                'hover:bg-base-800 active:bg-base-800' => $navigate,\n                'hover:border-base-800 focus:border-red' => $navigate,\n                'hover:text-base-50 active:text-base-50' => $navigate,\n            ])->when($navigate, fn ($bag) => $bag->merge([\n                'x-data' => Js::from(['navigate' => $navigate]),\n                'x-on:click' => 'current = navigate',\n            ]), fn ($bag) => $bag->merge([\n                'disabled' => '',\n            ]))\n        }}\n    >\n        <x-livewire-table::icon class=\"text-base-400 size-5 transition-colors duration-200\" :icon=\"$icon\" />\n        <span class=\"font-medium\">{{ $label }}</span>\n    </button>\n    {{ $slot }}\n</header>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/dropdown/index.blade.php",
    "content": "@props(['body', 'current'])\n\n<div\n    {{ $attributes }}\n    x-data=\"{\n        open: false,\n        toggle() {\n            this.open = ! this.open;\n        },\n        close() {\n            this.open = false;\n        },\n    }\"\n    x-on:click.away=\"close\"\n    x-on:keydown.escape.window=\"close\"\n>\n    {{ $slot }}\n    <div class=\"relative z-30\">\n        <div\n            x-data=\"@js(['current' => $current])\"\n            x-show=\"open\"\n            x-transition\n            x-cloak\n            class=\"absolute top-1 right-0 bg-base-850 shadow-lg rounded-lg w-64 border border-base-700 transition-all duration-200\"\n        >\n            {{ $body }}\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/dropdown/menu/index.blade.php",
    "content": "@if($slot->hasActualContent())\n    <ul {{ $attributes->class('py-1 transition') }}>\n        {{ $slot }}\n    </ul>\n@endif\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/dropdown/menu/item.blade.php",
    "content": "@props(['label', 'icon' => null, 'navigate' => null, 'dot' => false])\n\n<li>\n    <button\n        {{\n            $attributes->merge([\n                'type' => 'button',\n            ])->class([\n                'flex items-center gap-3 w-full px-4 py-2 text-left relative border group/item cursor-pointer transition-all duration-200 disabled:cursor-not-allowed',\n                'focus:outline-none focus:ring-2 focus:ring-red/50 focus:z-10',\n                'hover:bg-base-800 active:bg-base-800 disabled:hover:bg-base-850 disabled:active:bg-base-850',\n                'border-base-850 hover:border-base-800 focus:border-red disabled:hover:border-base-850',\n                'text-base-100 hover:text-base-50 active:text-base-50 disabled:text-base-500 disabled:hover:text-base-500',\n            ])->when($navigate, fn ($bag) => $bag->merge([\n                'x-data' => Js::from(['navigate' => $navigate]),\n                'x-on:click' => 'current = navigate',\n            ]))\n        }}\n    >\n        @if($icon)\n            <x-livewire-table::icon class=\"text-base-400 group-hover/item:text-base-300 group-active/item:text-base-300 transition-colors duration-200 size-5\" :icon=\"$icon\" />\n        @endif\n\n        <span class=\"relative flex-1\">\n            {{ $label }}\n        </span>\n\n        @if($navigate)\n            <x-livewire-table::icon class=\"text-base-400 group-hover/item:text-base-300 group-active/item:text-base-300 transition-colors duration-200 size-5\" icon=\"chevron-right\" />\n        @endif\n\n        @if($dot)\n            <span class=\"absolute left-8 top-2 rounded-full shadow-xs bg-red block size-2\"></span>\n        @endif\n    </button>\n</li>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/dropdown/section.blade.php",
    "content": "@props(['section'])\n\n<section\n    {{\n        $attributes->merge([\n            'x-data' => Js::from(['section' => $section]),\n            'x-show' => 'current === section',\n        ])->class([\n            'flex flex-col',\n        ])\n    }}\n>\n    {{ $slot }}\n</section>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/form/checkbox.blade.php",
    "content": "<label class=\"flex items-center relative\">\n    <input\n        {{\n            $attributes\n                ->merge(['type' => 'checkbox'])\n                ->class([\n                    'rounded-md size-5 cursor-pointer shadow-sm appearance-none border peer transition-all duration-200',\n                    'focus:outline-none focus:ring-2 focus:ring-red/50',\n                    'bg-base-850 checked:bg-red',\n                    'border-base-700 focus:border-red checked:border-red',\n                ])\n        }}\n    />\n    <span class=\"absolute text-base-50 opacity-0 peer-checked:opacity-100 top-0.5 left-0.5 transform pointer-events-none transition-opacity duration-200\">\n        <x-livewire-table::icon icon=\"check\" class=\"size-4\" />\n    </span>\n</label>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/form/group.blade.php",
    "content": "@props(['label'])\n\n<div class=\"px-4 py-3 transition-all duration-200\">\n    <label class=\"flex flex-col gap-0.5\">\n        <span class=\"block uppercase text-xs font-bold whitespace-nowrap truncate text-base-400 transition-colors duration-200\" title=\"{{ $label }}\">{{ $label }}</span>\n        {{ $slot }}\n    </label>\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/form/input.blade.php",
    "content": "@props(['size' => 'md'])\n\n<input\n    {{\n        $attributes->class([\n            'w-full rounded-lg border transition-all duration-200',\n            'focus:outline-none focus:ring-2 focus:ring-red/50 focus:z-10',\n            'bg-base-800 hover:bg-base-700 active:bg-base-700',\n            'border-base-700 hover:border-base-600 focus:border-red',\n            'text-base-100 placeholder:text-base-400',\n            'px-3 py-2' => $size === 'md',\n            'px-2 py-1' => $size === 'sm',\n        ])\n    }}\n/>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/form/select.blade.php",
    "content": "@props(['size' => 'md'])\n\n<label class=\"block relative\">\n    <select\n        {{\n            $attributes->class([\n                'w-full rounded-lg border appearance-none transition-all duration-200',\n                'focus:outline-none focus:ring-2 focus:ring-red/50 focus:z-10',\n                'bg-base-800 hover:bg-base-700 active:bg-base-700',\n                'border-base-700 hover:border-base-600 focus:border-red',\n                'text-base-100',\n                'pl-3 pr-10 py-2' => $size === 'md',\n                'pl-2 pr-6 py-1' => $size === 'sm',\n            ])\n        }}\n    >\n        {{ $slot }}\n    </select>\n    @if($size === 'sm')\n        <x-livewire-table::icon icon=\"chevron-down\" class=\"pointer-events-none size-3 absolute right-2 top-3 text-base-100 transition-colors duration-200\" />\n    @elseif($size === 'md')\n        <x-livewire-table::icon icon=\"chevron-down\" class=\"pointer-events-none size-4.5 absolute right-3 top-3 text-base-100 transition-colors duration-200\" />\n    @endif\n</label>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/icon.blade.php",
    "content": "@props(['icon'])\n\n<span {{ $attributes->class('block') }}>\n    @include('livewire-table::icons.'.$icon)\n</span>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/notification/button.blade.php",
    "content": "@props(['variant' => 'info'])\n\n<button\n    {{\n        $attributes->merge([\n            'type' => 'button',\n        ])->class([\n            'relative flex items-center px-3 py-2 h-full border cursor-pointer transition',\n            'focus:outline-none focus:ring focus:z-10',\n\n            'ring-blue-300 dark:ring-blue-400' => $variant === 'info',\n            'bg-blue-50 dark:bg-blue-900 hover:bg-blue-100 dark:hover:bg-blue-700 active:bg-blue-100 dark:active:bg-blue-700' => $variant === 'info',\n            'border-blue-50 dark:border-blue-900 hover:border-blue-100 dark:hover:border-blue-700 focus:border-blue-300 dark:focus:border-blue-400' => $variant === 'info',\n            'text-blue-600 dark:text-blue-300 hover:text-blue-700 dark:hover:text-blue-200 active:text-blue-700 dark:active:text-blue-200' => $variant === 'info',\n        ])\n    }}\n>\n    {{ $slot }}\n</button>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/notification/index.blade.php",
    "content": "@props(['icon', 'label'])\n\n<div class=\"flex gap-3 rounded-md bg-blue-50 dark:bg-blue-900 text-blue-600 dark:text-blue-300 transition\">\n    <p class=\"flex items-center gap-2 flex-1 px-3 py-2\">\n        <x-livewire-table::icon :icon=\"$icon\" class=\"shrink-0 size-5\" />\n        {{ $label }}\n    </p>\n    <div class=\"flex items-stretch\">\n        {{ $slot }}\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/table/index.blade.php",
    "content": "<table {{ $attributes->class('w-full relative') }}>\n    {{ $slot }}\n</table>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/table/message.blade.php",
    "content": "<p {{ $attributes->class('px-3 py-20 text-center text-base-300 transition-all duration-200') }}>\n    {{ $slot }}\n</p>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/table/tbody.blade.php",
    "content": "<tbody {{ $attributes }}>\n    {{ $slot }}\n</tbody>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/table/td.blade.php",
    "content": "<td {{ $attributes->class('px-4 py-4 text-base-200 max-w-0') }}>{{ $slot }}</td>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/table/tfoot.blade.php",
    "content": "<tfoot {{ $attributes->class('bg-base-900 border-t border-base-700 sticky bottom-0 shadow-lg z-20 transition-all duration-200') }}>\n    {{ $slot }}\n</tfoot>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/table/th.blade.php",
    "content": "<th {{ $attributes->class('px-4 py-2 text-left text-sm font-semibold text-base-100 uppercase tracking-wider transition-all duration-200') }}>\n    {{ $slot }}\n</th>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/table/thead.blade.php",
    "content": "<thead {{ $attributes->class('bg-base-850 border-b-2 border-base-700 sticky top-0 shadow-lg z-20 transition-all duration-200') }}>\n    {{ $slot }}\n</thead>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/components/table/tr.blade.php",
    "content": "<tr {{ $attributes }}>{{ $slot }}</tr>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/filters/boolean.blade.php",
    "content": "<x-livewire-table::form.group :label=\"$filter->label()\">\n    <x-livewire-table::form.select wire:model.live=\"filters.{{ $filter->code() }}\">\n        <option value=\"\">&mdash;</option>\n        <option value=\"1\">@lang('Yes')</option>\n        <option value=\"0\">@lang('No')</option>\n    </x-livewire-table::form.select>\n</x-livewire-table::form.group>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/filters/date.blade.php",
    "content": "<x-livewire-table::dropdown.divider>\n    <x-livewire-table::form.group :label=\"$filter->label().' (from)'\">\n        <x-livewire-table::form.input type=\"date\" wire:model.live=\"filters.{{ $filter->code() }}.from\" />\n    </x-livewire-table::form.group>\n    <x-livewire-table::form.group :label=\"$filter->label().' (to)'\">\n        <x-livewire-table::form.input type=\"date\" wire:model.live=\"filters.{{ $filter->code() }}.to\" />\n    </x-livewire-table::form.group>\n</x-livewire-table::dropdown.divider>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/filters/filter.blade.php",
    "content": ""
  },
  {
    "path": "resources/views/vendor/livewire-table/filters/select.blade.php",
    "content": "<x-livewire-table::form.group :label=\"$filter->label()\">\n    @if($filter->isMultiple())\n        <x-livewire-table::form.select wire:model.live=\"filters.{{ $filter->code() }}\" multiple>\n            @foreach($filter->getOptions() as $key => $value)\n                @if(is_array($value))\n                    <optgroup wire:key=\"{{ $key }}\" label=\"{{ $key }}\">\n                        @foreach($value as $key2 => $value2)\n                            <option wire:key=\"{{ $key2 }}\" value=\"{{ $key2 }}\">{{ $value2 }}</option>\n                        @endforeach\n                    </optgroup>\n                @else\n                    <option wire:key=\"{{ $key }}\" value=\"{{ $key }}\">{{ $value }}</option>\n                @endif\n            @endforeach\n        </x-livewire-table::form.select>\n    @else\n        <x-livewire-table::form.select wire:model.live=\"filters.{{ $filter->code() }}\">\n            <option value=\"\">&mdash;</option>\n            @foreach($filter->getOptions() as $key => $value)\n                @if(is_array($value))\n                    <optgroup wire:key=\"{{ $key }}\" label=\"{{ $key }}\">\n                        @foreach($value as $key2 => $value2)\n                            <option wire:key=\"{{ $key2 }}\" value=\"{{ $key2 }}\">{{ $value2 }}</option>\n                        @endforeach\n                    </optgroup>\n                @else\n                    <option wire:key=\"{{ $key }}\" value=\"{{ $key }}\">{{ $value }}</option>\n                @endif\n            @endforeach\n        </x-livewire-table::form.select>\n    @endif\n</x-livewire-table::form.group>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/arrow-path.blade.php",
    "content": "<!-- Icon \"arrow-path\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/backspace.blade.php",
    "content": "<!-- Icon \"backspace\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 9.75 14.25 12m0 0 2.25 2.25M14.25 12l2.25-2.25M14.25 12 12 14.25m-2.58 4.92-6.374-6.375a1.125 1.125 0 0 1 0-1.59L9.42 4.83c.21-.211.497-.33.795-.33H19.5a2.25 2.25 0 0 1 2.25 2.25v10.5a2.25 2.25 0 0 1-2.25 2.25h-9.284c-.298 0-.585-.119-.795-.33Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/check-circle.blade.php",
    "content": "<!-- Icon \"check-circle\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/check.blade.php",
    "content": "<!-- Icon \"check\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4.5 12.75 6 6 9-13.5\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/chevron-down.blade.php",
    "content": "<!-- Icon \"chevron-down\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m19.5 8.25-7.5 7.5-7.5-7.5\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/chevron-left.blade.php",
    "content": "<!-- Icon \"chevron-left\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15.75 19.5 8.25 12l7.5-7.5\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/chevron-right.blade.php",
    "content": "<!-- Icon \"chevron-right\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m8.25 4.5 7.5 7.5-7.5 7.5\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/chevron-up-down.blade.php",
    "content": "<!-- Icon \"chevron-up-down\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/chevron-up.blade.php",
    "content": "<!-- Icon \"chevron-up\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4.5 15.75 7.5-7.5 7.5 7.5\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/clipboard-document-check.blade.php",
    "content": "<!-- Icon \"clipboard-document-check\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/clipboard-document.blade.php",
    "content": "<!-- Icon \"clipboard-document\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5A3.375 3.375 0 0 0 6.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0 0 15 2.25h-1.5a2.251 2.251 0 0 0-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 0 0-9-9Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/clock.blade.php",
    "content": "<!-- Icon \"clock\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n  <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/cog-6-tooth.blade.php",
    "content": "<!-- Icon \"cog-6-tooth\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z\" />\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/ellipsis-vertical.blade.php",
    "content": "<!-- Icon \"ellipsis-vertical\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/eye.blade.php",
    "content": "<!-- Icon \"eye\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z\" />\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/funnel.blade.php",
    "content": "<!-- Icon \"funnel\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/information-circle.blade.php",
    "content": "<!-- Icon \"information-circle\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/list-bullet.blade.php",
    "content": "<!-- Icon \"list-bullet\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/magnifying-glass.blade.php",
    "content": "<!-- Icon \"magnifying-glass\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/min.blade.php",
    "content": "<!-- Icon \"min\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5 12h14\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/play.blade.php",
    "content": "<!-- Icon \"play\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/plus.blade.php",
    "content": "<!-- Icon \"plus\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 4.5v15m7.5-7.5h-15\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/queue-list.blade.php",
    "content": "<!-- Icon \"queue-list\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 0 1 0 3.75H5.625a1.875 1.875 0 0 1 0-3.75Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/view-columns.blade.php",
    "content": "<!-- Icon \"view-columns\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 4.5v15m6-15v15m-10.875 0h15.75c.621 0 1.125-.504 1.125-1.125V5.625c0-.621-.504-1.125-1.125-1.125H4.125C3.504 4.5 3 5.004 3 5.625v12.75c0 .621.504 1.125 1.125 1.125Z\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/icons/x-mark.blade.php",
    "content": "<!-- Icon \"x-mark\" (outline) from https://heroicons.com -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18 18 6M6 6l12 12\" />\n</svg>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/livewire/livewire-table.blade.php",
    "content": "<div\n    class=\"flex flex-col gap-3 relative\"\n    @if($this->deferLoading) wire:init=\"init\" @endif\n    @if(strlen($polling = $this->polling()) > 0) wire:poll.{{ $polling }} @endif\n>\n    <div class=\"bg-base-850 rounded-lg shadow-lg flex flex-col border border-base-700 transition-all duration-200\">\n        @include('livewire-table::toolbar.toolbar')\n        <div class=\"flex-1 overflow-x-auto overflow-y-auto max-h-179 rounded-b-lg\">\n            @include('livewire-table::table.table')\n        </div>\n    </div>\n    {{ $paginator->onEachSide(1)->links('livewire-table::pagination.pagination') }}\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/pagination/pagination.blade.php",
    "content": "@php\nif (! isset($scrollTo)) {\n    $scrollTo = 'body';\n}\n\n$scrollIntoViewJsSnippet = ($scrollTo !== false)\n    ? <<<JS\n       (\\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()\n    JS\n    : '';\n@endphp\n\n<div>\n    @if($paginator->hasPages())\n        <nav role=\"navigation\" aria-label=\"Pagination Navigation\" class=\"flex items-center justify-between\">\n            <div class=\"flex justify-between flex-1 sm:hidden\">\n                <span>\n                    @if($paginator->onFirstPage())\n                        <span class=\"relative inline-flex items-center px-4 py-2 text-sm font-medium text-base-500 bg-base-850 border border-base-700 cursor-default leading-5 rounded-lg transition-all duration-200\">\n                            {!! __('pagination.previous') !!}\n                        </span>\n                    @else\n                        <button type=\"button\" wire:click=\"previousPage('{{ $paginator->getPageName() }}')\" x-on:click=\"{{ $scrollIntoViewJsSnippet }}\" wire:loading.attr=\"disabled\" dusk=\"previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.before\" class=\"relative inline-flex items-center px-4 py-2 text-sm font-medium text-base-100 bg-base-850 border border-base-700 leading-5 rounded-lg hover:bg-base-800 hover:text-base-50 hover:border-base-600 focus:outline-none focus:ring-2 focus:ring-red/50 focus:border-red active:bg-base-800 transition-all duration-200\">\n                            {!! __('pagination.previous') !!}\n                        </button>\n                    @endif\n                </span>\n\n                <span>\n                    @if($paginator->hasMorePages())\n                        <button type=\"button\" wire:click=\"nextPage('{{ $paginator->getPageName() }}')\" x-on:click=\"{{ $scrollIntoViewJsSnippet }}\" wire:loading.attr=\"disabled\" dusk=\"nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.before\" class=\"relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-base-100 bg-base-850 border border-base-700 leading-5 rounded-lg hover:bg-base-800 hover:text-base-50 hover:border-base-600 focus:outline-none focus:ring-2 focus:ring-red/50 focus:border-red active:bg-base-800 transition-all duration-200\">\n                            {!! __('pagination.next') !!}\n                        </button>\n                    @else\n                        <span class=\"relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-base-500 bg-base-850 border border-base-700 cursor-default leading-5 rounded-lg transition-all duration-200\">\n                            {!! __('pagination.next') !!}\n                        </span>\n                    @endif\n                </span>\n            </div>\n\n            <div class=\"hidden sm:flex-1 sm:flex sm:items-center sm:justify-between\">\n                <div>\n                    <p class=\"transition-all duration-200 text-sm text-base-300 leading-5\">\n                        <span>{!! __('Showing') !!}</span>\n                        <span class=\"font-medium text-base-100\">{{ $paginator->firstItem() }}</span>\n                        <span>{!! __('to') !!}</span>\n                        <span class=\"font-medium text-base-100\">{{ $paginator->lastItem() }}</span>\n                        <span>{!! __('of') !!}</span>\n                        <span class=\"font-medium text-base-100\">{{ $paginator->total() }}</span>\n                        <span>{!! __('results') !!}</span>\n                    </p>\n                </div>\n\n                <div>\n                    <span class=\"relative z-0 inline-flex rtl:flex-row-reverse rounded-lg shadow-sm\">\n                        <span>\n                            {{-- Previous Page Link --}}\n                            @if($paginator->onFirstPage())\n                                <span aria-disabled=\"true\" aria-label=\"{{ __('pagination.previous') }}\">\n                                    <span class=\"transition-all duration-200 relative inline-flex items-center px-2 py-2 text-sm font-medium text-base-500 bg-base-850 border border-base-700 cursor-default rounded-l-lg leading-5\" aria-hidden=\"true\">\n                                        <x-livewire-table::icon icon=\"chevron-left\" class=\"size-5\" />\n                                    </span>\n                                </span>\n                            @else\n                                <button type=\"button\" wire:click=\"previousPage('{{ $paginator->getPageName() }}')\" x-on:click=\"{{ $scrollIntoViewJsSnippet }}\" dusk=\"previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.after\" class=\"relative inline-flex items-center px-2 py-2 text-sm font-medium text-base-200 bg-base-850 border border-base-700 rounded-l-lg leading-5 hover:text-base-50 hover:bg-base-800 hover:border-base-600 focus:z-10 focus:outline-none focus:border-red focus:ring-2 focus:ring-red/50 active:bg-base-800 transition-all duration-200\" aria-label=\"{{ __('pagination.previous') }}\">\n                                    <x-livewire-table::icon icon=\"chevron-left\" class=\"size-5\" />\n                                </button>\n                            @endif\n                        </span>\n\n                        {{-- Pagination Elements --}}\n                        @foreach($elements as $element)\n                            {{-- \"Three Dots\" Separator --}}\n                            @if(is_string($element))\n                                <span aria-disabled=\"true\">\n                                    <span class=\"transition-all duration-200 relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-base-300 bg-base-850 border border-base-700 cursor-default leading-5\">{{ $element }}</span>\n                                </span>\n                            @endif\n\n                            {{-- Array Of Links --}}\n                            @if(is_array($element))\n                                @foreach($element as $page => $url)\n                                    <span wire:key=\"paginator-{{ $paginator->getPageName() }}-page{{ $page }}\">\n                                        @if($page == $paginator->currentPage())\n                                            <span aria-current=\"page\">\n                                                <span class=\"transition-all duration-200 relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-base-50 bg-red border border-red cursor-default leading-5 font-semibold\">{{ $page }}</span>\n                                            </span>\n                                        @else\n                                            <button type=\"button\" wire:click=\"gotoPage({{ $page }}, '{{ $paginator->getPageName() }}')\" x-on:click=\"{{ $scrollIntoViewJsSnippet }}\" class=\"relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-base-200 bg-base-850 border border-base-700 leading-5 hover:text-base-50 hover:bg-base-800 hover:border-base-600 focus:z-10 focus:outline-none focus:border-red focus:ring-2 focus:ring-red/50 active:bg-base-800 transition-all duration-200\" aria-label=\"{{ __('Go to page :page', ['page' => $page]) }}\">\n                                                {{ $page }}\n                                            </button>\n                                        @endif\n                                    </span>\n                                @endforeach\n                            @endif\n                        @endforeach\n\n                        <span>\n                            {{-- Next Page Link --}}\n                            @if($paginator->hasMorePages())\n                                <button type=\"button\" wire:click=\"nextPage('{{ $paginator->getPageName() }}')\" x-on:click=\"{{ $scrollIntoViewJsSnippet }}\" dusk=\"nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.after\" class=\"relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-base-200 bg-base-850 border border-base-700 rounded-r-lg leading-5 hover:text-base-50 hover:bg-base-800 hover:border-base-600 focus:z-10 focus:outline-none focus:border-red focus:ring-2 focus:ring-red/50 active:bg-base-800 transition-all duration-200\" aria-label=\"{{ __('pagination.next') }}\">\n                                    <x-livewire-table::icon icon=\"chevron-right\" class=\"size-5\" />\n                                </button>\n                            @else\n                                <span aria-disabled=\"true\" aria-label=\"{{ __('pagination.next') }}\">\n                                    <span class=\"transition-all duration-200 relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-base-500 bg-base-850 border border-base-700 cursor-default rounded-r-lg leading-5\" aria-hidden=\"true\">\n                                        <x-livewire-table::icon icon=\"chevron-right\" class=\"size-5\" />\n                                    </span>\n                                </span>\n                            @endif\n                        </span>\n                    </span>\n                </div>\n            </div>\n        </nav>\n    @endif\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/table/table.blade.php",
    "content": "@php($columns = $this->resolveColumns())\n\n<x-livewire-table::table x-data=\"{ selected: $wire.entangle('selected') }\">\n    <x-livewire-table::table.thead>\n        <x-livewire-table::table.tr>\n            @if($this->canSelect())\n                <x-livewire-table::table.th class=\"px-3\">\n                    <x-livewire-table::form.checkbox wire:model.live=\"selectedPage\" />\n                </x-livewire-table::table.th>\n            @endif\n            @foreach($columns as $column)\n                @continue(! in_array($column->code(), $this->columns))\n                <x-livewire-table::table.th wire:key=\"{{ $column->code() }}\">\n                    {{ $column->renderHeader() }}\n                </x-livewire-table::table.th>\n            @endforeach\n        </x-livewire-table::table.tr>\n        @if($this->canSearch())\n            <x-livewire-table::table.tr class=\"h-12\">\n                @if($this->canSelect())\n                    <x-livewire-table::table.td class=\"!py-0\" />\n                @endif\n                @foreach($columns as $column)\n                    @continue(! in_array($column->code(), $this->columns))\n                    <x-livewire-table::table.td class=\"!py-0\" wire:key=\"{{ $column->code() }}\">\n                        @if($column->isSearchable())\n                            {{ $column->renderSearch() }}\n                        @endif\n                    </x-livewire-table::table.td>\n                @endforeach\n            </x-livewire-table::table.td>\n        @endif\n    </x-livewire-table::table.thead>\n\n    <x-livewire-table::table.tbody>\n        @if($this->deferLoading && ! $this->initialized)\n            @for($i = 0; $i < $this->perPage(); $i++)\n                <x-livewire-table::table.tr\n                    wire:key=\"placeholder-{{ $i }}\"\n                    class=\"bg-base-900 odd:bg-base-900/50 hover:bg-base-800/80 transition-all duration-200 motion-safe:animate-pulse border-b border-base-700/50\"\n                >\n                    @if($this->canSelect())\n                        <x-livewire-table::table.td class=\"px-3\">\n                            <x-livewire-table::form.checkbox disabled />\n                        </x-livewire-table::table.td>\n                    @endif\n                    @foreach($columns as $column)\n                        @continue(! in_array($column->code(), $this->columns))\n                        <td wire:key=\"{{ $column->code() }}\">\n                            <div class=\"px-4 py-3\">\n                                <span class=\"block w-full rounded-full min-w-8 h-2 my-2 bg-base-700 transition-all duration-200\"></span>\n                            </div>\n                        </td>\n                    @endforeach\n                </x-livewire-table::table.tr>\n            @endfor\n        @else\n            @forelse($paginator->items() as $item)\n                <tr\n                    x-data=\"@js(['item' => (string) $item->getKey()])\"\n                    x-bind:class=\"~selected.indexOf(item)\n                        ? 'bg-red/10 hover:bg-red/20 transition-all duration-200 border-l-4 border-red shadow-sm shadow-red/20'\n                        : 'bg-base-900 odd:bg-base-900/50 hover:bg-base-800/80 transition-all duration-200 border-b border-base-700/50'\n                    \"\n                    wire:key=\"row-{{ $item->getKey() }}\"\n\n                    @if($this->isReordering())\n                        draggable=\"true\"\n                        x-on:dragstart=\"e => e.dataTransfer.setData('key', item)\"\n                        x-on:dragover.prevent=\"\"\n                        x-on:drop=\"e => {\n                            $wire.call(\n                                'reorderItem',\n                                e.dataTransfer.getData('key'),\n                                item,\n                                e.target.offsetHeight / 2 > e.offsetY\n                            )\n                        }\"\n                    @endif\n                >\n                    @if($this->canSelect())\n                        <x-livewire-table::table.td class=\"px-3\">\n                            <x-livewire-table::form.checkbox x-ref=\"checkbox\" wire:model=\"selected\" value=\"{{ $item->getKey() }}\" />\n                        </x-livewire-table::table.td>\n                    @endif\n                    @foreach($columns as $column)\n                        @continue(! in_array($column->code(), $this->columns))\n                        <td\n                            wire:key=\"{{ $column->code() }}\"\n                            @class([\n                                'group/column relative' => true,\n                                'select-none cursor-pointer' => $column->isClickable() || $this->isReordering(),\n                            ])\n                            @if($column->isClickable() && ! $this->isReordering())\n                                @if(($link = $this->link($item)) !== null)\n                                    @if($this->useNavigate)\n                                        x-on:click.prevent=\"Livewire.navigate(@js($link))\"\n                                    @else\n                                        x-on:click.prevent=\"window.location.href = @js($link)\"\n                                    @endif\n                                @elseif($this->canSelect())\n                                    x-on:click=\"$refs.checkbox.click()\"\n                                @endif\n                            @endif\n                        >\n                            @includeWhen($column->isCopyable(), 'livewire-table::columns.buttons.copy')\n                            <div x-ref=\"content\">\n                                {{ $column->render($item) }}\n                            </div>\n                        </td>\n                    @endforeach\n                </tr>\n            @empty\n                <x-livewire-table::table.tr class=\"bg-base-900/50 transition-all duration-200\">\n                    <x-livewire-table::table.td colspan=\"{{ $columns->count() + 1 }}\" class=\"text-center py-12\">\n                        <x-livewire-table::table.message>\n                            @lang('No results')\n                        </x-livewire-table::table.message>\n                    </x-livewire-table::table.td>\n                </x-livewire-table::table.tr>\n            @endforelse\n        @endif\n    </x-livewire-table::table.tbody>\n\n    <x-livewire-table::table.tfoot>\n        <x-livewire-table::table.tr>\n            @if($this->canSelect())\n                <x-livewire-table::table.th />\n            @endif\n            @foreach($columns as $column)\n                @continue(! in_array($column->code(), $this->columns))\n                <x-livewire-table::table.th wire:key=\"{{ $column->code() }}\">\n                    {{ $column->renderFooter() }}\n                </x-livewire-table::table.th>\n            @endforeach\n        </x-livewire-table::table.tr>\n    </x-livewire-table::table.tfoot>\n</x-livewire-table::table>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/buttons/clear-search.blade.php",
    "content": "<x-livewire-table::button\n    :title=\"__('Clear search')\"\n    :aria-label=\"__('Clear search')\"\n    wire:click=\"clearSearch\"\n>\n    <x-livewire-table::icon class=\"size-6\" icon=\"backspace\" />\n</x-livewire-table::button>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/buttons/reordering.blade.php",
    "content": "<x-livewire-table::button\n    :title=\"__('Reordering')\"\n    :aria-label=\"__('Reordering')\"\n    wire:click=\"$toggle('reordering')\"\n    :active=\"$this->isReordering()\"\n>\n    <x-livewire-table::icon class=\"size-6\" icon=\"queue-list\" />\n</x-livewire-table::button>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/dropdowns/actions.blade.php",
    "content": "<x-livewire-table::dropdown current=\"actions\">\n    <x-livewire-table::button\n        :title=\"__('Actions')\"\n        :aria-label=\"__('Actions')\"\n        x-on:click=\"toggle\"\n    >\n        <x-livewire-table::icon class=\"size-6\" icon=\"play\" />\n    </x-livewire-table::button>\n    <x-slot:body>\n        @include('livewire-table::toolbar.dropdowns.sections.actions')\n    </x-slot:body>\n</x-livewire-table::dropdown>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/dropdowns/configuration.blade.php",
    "content": "@php($columns = $this->resolveColumns())\n@php($filters = $this->resolveFilters())\n\n<x-livewire-table::dropdown current=\"configuration\">\n    <x-livewire-table::button\n        :title=\"__('Configuration')\"\n        :aria-label=\"__('Configuration')\"\n        x-on:click=\"toggle\"\n        :dot=\"$this->canClearFilters()\"\n    >\n        <x-livewire-table::icon class=\"size-6\" icon=\"cog-6-tooth\" />\n    </x-livewire-table::button>\n    <x-slot:body>\n        @include('livewire-table::toolbar.dropdowns.sections.configuration')\n        @includeWhen($columns->isNotEmpty(), 'livewire-table::toolbar.dropdowns.sections.columns')\n        @includeWhen($filters->isNotEmpty(), 'livewire-table::toolbar.dropdowns.sections.filters')\n        @includeWhen($this->hasSoftDeletes(), 'livewire-table::toolbar.dropdowns.sections.trashed')\n        @include('livewire-table::toolbar.dropdowns.sections.results')\n        @includeWhen(count($pollingOptions) > 0, 'livewire-table::toolbar.dropdowns.sections.polling')\n    </x-slot:body>\n</x-livewire-table::dropdown>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/dropdowns/sections/actions.blade.php",
    "content": "@php($actions = $this->resolveActions())\n\n<x-livewire-table::dropdown.section section=\"actions\">\n    <x-livewire-table::dropdown.header :label=\"__('Actions')\" icon=\"play\" />\n    <x-livewire-table::dropdown.content>\n        @php($standaloneActions = $actions->standalone())\n        @if($standaloneActions->isNotEmpty())\n            <x-livewire-table::dropdown.menu>\n                @foreach($standaloneActions as $standaloneAction)\n                    @if($standaloneAction->isScript())\n                        <x-livewire-table::dropdown.menu.item\n                            :label=\"$standaloneAction->label()\"\n                            wire:key=\"{{ $standaloneAction->code() }}\"\n                            x-on:click=\"\n                                {{ $standaloneAction->script() }}\n                                close()\n                            \"\n                        />\n                    @else\n                        <x-livewire-table::dropdown.menu.item\n                            :label=\"$standaloneAction->label()\"\n                            wire:key=\"{{ $standaloneAction->code() }}\"\n                            wire:click=\"executeAction({{ Js::from($standaloneAction->code()) }})\"\n                            x-on:click=\"close\"\n                        />\n                    @endif\n                @endforeach\n            </x-livewire-table::dropdown.menu>\n        @endif\n        @php($bulkActions = $actions->bulk())\n        @if($bulkActions->isNotEmpty())\n            <x-livewire-table::dropdown.menu x-data=\"{ selected: $wire.entangle('selected') }\">\n                @foreach($bulkActions as $bulkAction)\n                    @if($bulkAction->isScript())\n                        <x-livewire-table::dropdown.menu.item\n                            :label=\"$bulkAction->label()\"\n                            wire:key=\"{{ $bulkAction->code() }}\"\n                            x-bind:disabled=\"selected.length === 0\"\n                            x-on:click=\"\n                                {{ $bulkAction->script() }}\n                                close()\n                            \"\n                        />\n                    @else\n                        <x-livewire-table::dropdown.menu.item\n                            :label=\"$bulkAction->label()\"\n                            wire:key=\"{{ $bulkAction->code() }}\"\n                            x-bind:disabled=\"selected.length === 0\"\n                            wire:click=\"executeAction({{ Js::from($bulkAction->code()) }})\"\n                            x-on:click=\"close\"\n                        />\n                    @endif\n                @endforeach\n            </x-livewire-table::dropdown.menu>\n        @endif\n    </x-livewire-table::dropdown.content>\n</x-livewire-table::dropdown.section>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/dropdowns/sections/columns.blade.php",
    "content": "@php($columns = $this->resolveColumns())\n\n<x-livewire-table::dropdown.section section=\"columns\">\n    <x-livewire-table::dropdown.header :label=\"__('Columns')\" icon=\"chevron-left\" navigate=\"configuration\" />\n    <x-livewire-table::dropdown.content>\n        <x-livewire-table::dropdown.menu>\n            @foreach($columns as $column)\n                <li>\n                    <label\n                        wire:key=\"{{ $column->code() }}\"\n                        class=\"flex items-center gap-2 px-4 py-1 cursor-grab text-gray-700 dark:text-gray-400 hover:text-gray-800 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 transition\"\n                        draggable=\"true\"\n                        x-on:dragstart=\"e => e.dataTransfer.setData('code', @js($column->code()))\"\n                        x-on:dragover.prevent=\"\"\n                        x-on:drop=\"e => {\n                            $wire.call(\n                                'reorderColumn',\n                                e.dataTransfer.getData('code'),\n                                @js($column->code()),\n                                e.target.offsetHeight / 2 > e.offsetY\n                            )\n                        }\"\n                    >\n                        <x-livewire-table::form.checkbox value=\"{{ $column->code() }}\" wire:model.live=\"columns\" />\n                        <span class=\"truncate\" title=\"{{ $column->label() }}\">{{ $column->label() }}</span>\n                    </label>\n                </li>\n            @endforeach\n        </x-livewire-table::dropdown.menu>\n        <x-livewire-table::dropdown.footer>\n            @lang('Select the columns you want to see. Drag them to change order.')\n        </x-livewire-table::dropdown.footer>\n    </x-livewire-table::dropdown.content>\n</x-livewire-table::dropdown.section>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/dropdowns/sections/configuration.blade.php",
    "content": "@php($columns = $this->resolveColumns())\n@php($filters = $this->resolveFilters())\n\n<x-livewire-table::dropdown.section section=\"configuration\">\n    <x-livewire-table::dropdown.header :label=\"__('Configuration')\" icon=\"cog-6-tooth\" />\n    <x-livewire-table::dropdown.content>\n        <x-livewire-table::dropdown.menu>\n            @if($columns->isNotEmpty())\n                <x-livewire-table::dropdown.menu.item :label=\"__('Columns')\" icon=\"view-columns\" navigate=\"columns\" />\n            @endif\n            @if($filters->isNotEmpty())\n                <x-livewire-table::dropdown.menu.item :label=\"__('Filters')\" icon=\"funnel\" navigate=\"filters\" :dot=\"$this->canClearFilters()\" />\n            @endif\n            @if($this->hasSoftDeletes())\n                <x-livewire-table::dropdown.menu.item :label=\"__('Visibility')\" icon=\"eye\" navigate=\"trashed\" />\n            @endif\n        </x-livewire-table::dropdown.menu>\n        <x-livewire-table::dropdown.menu>\n            <x-livewire-table::dropdown.menu.item :label=\"__('Results')\" icon=\"list-bullet\" navigate=\"results\" />\n            @if(count($pollingOptions) > 0)\n                <x-livewire-table::dropdown.menu.item :label=\"__('Reload')\" icon=\"clock\" navigate=\"polling\" />\n            @endif\n        </x-livewire-table::dropdown.menu>\n        <x-livewire-table::dropdown.menu>\n            <x-livewire-table::dropdown.menu.item :label=\"__('Refresh')\" icon=\"arrow-path\" wire:click=\"$refresh\" x-on:click=\"close\" />\n        </x-livewire-table::dropdown.menu>\n    </x-livewire-table::dropdown.content>\n</x-livewire-table::dropdown.section>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/dropdowns/sections/filters.blade.php",
    "content": "@php($filters = $this->resolveFilters())\n\n<x-livewire-table::dropdown.section section=\"filters\">\n    <x-livewire-table::dropdown.header :label=\"__('Filters')\" icon=\"chevron-left\" navigate=\"configuration\">\n        @if($this->canClearFilters())\n            <button\n                @class([\n                    'flex items-center gap-3 px-4 py-3 text-sm border cursor-pointer rounded-tr-md transition',\n                    'ring-red-300 dark:ring-red-400',\n                    'focus:outline-none focus:ring focus:z-10',\n                    'bg-gray-50 dark:bg-gray-700 hover:bg-red-50 dark:hover:bg-gray-600 active:bg-red-50 dark:active:bg-gray-600',\n                    'border-gray-50 dark:border-gray-700 hover:border-red-50 dark:hover:border-gray-600 focus:border-red-300 dark:focus:border-red-400',\n                    'text-red-600 dark:text-red-500',\n                ])\n                type=\"button\"\n                title=\"@lang('Reset the filters')\"\n                aria-label=\"@lang('Reset the filters')\"\n                wire:click=\"clearFilters\"\n                x-on:click=\"close\"\n            >\n                @lang('Reset')\n            </button>\n        @endif\n    </x-livewire-table::dropdown.header>\n    <x-livewire-table::dropdown.content>\n        @foreach($filters as $filter)\n            <div wire:key=\"{{ $filter->code() }}\" class=\"transition\">\n                {{ $filter->render() }}\n            </div>\n        @endforeach\n        <x-livewire-table::dropdown.footer>\n            @lang('Enable filters to narrow down your results.')\n        </x-livewire-table::dropdown.footer>\n    </x-livewire-table::dropdown.content>\n</x-livewire-table::dropdown.section>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/dropdowns/sections/polling.blade.php",
    "content": "<x-livewire-table::dropdown.section section=\"polling\">\n    <x-livewire-table::dropdown.header :label=\"__('Reload')\" icon=\"chevron-left\" navigate=\"configuration\" />\n    <x-livewire-table::dropdown.content>\n        <x-livewire-table::form.group :label=\"__('Interval')\">\n            <x-livewire-table::form.select name=\"polling\" wire:model.live=\"polling\">\n                @foreach($pollingOptions as $key => $value)\n                    <option wire:key=\"{{ $key }}\" value=\"{{ $key }}\">@lang($value)</option>\n                @endforeach\n            </x-livewire-table::form.select>\n        </x-livewire-table::form.group>\n        <x-livewire-table::dropdown.footer>\n            @lang('Automatically refresh the table with the given interval.')\n        </x-livewire-table::dropdown.footer>\n    </x-livewire-table::dropdown.content>\n</x-livewire-table::dropdown.section>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/dropdowns/sections/results.blade.php",
    "content": "<x-livewire-table::dropdown.section section=\"results\">\n    <x-livewire-table::dropdown.header :label=\"__('Results')\" icon=\"chevron-left\" navigate=\"configuration\" />\n    <x-livewire-table::dropdown.content>\n        <x-livewire-table::form.group :label=\"__('Records')\">\n            <x-livewire-table::form.select name=\"perPage\" wire:model.live=\"perPage\">\n                @foreach($perPageOptions as $perPage)\n                    <option wire:key=\"{{ $perPage }}\" value=\"{{ $perPage }}\">{{ $perPage }}</option>\n                @endforeach\n            </x-livewire-table::form.select>\n        </x-livewire-table::form.group>\n        <x-livewire-table::dropdown.footer>\n            @lang('Change the amount of visible records in the table.')\n        </x-livewire-table::dropdown.footer>\n    </x-livewire-table::dropdown.content>\n</x-livewire-table::dropdown.section>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/dropdowns/sections/suggestions.blade.php",
    "content": "<x-livewire-table::dropdown.section section=\"suggestions\">\n    <x-livewire-table::dropdown.header :label=\"__('Suggestions')\" icon=\"ellipsis-vertical\" />\n    <x-livewire-table::dropdown.content>\n        <x-livewire-table::dropdown.menu>\n            <x-livewire-table::dropdown.menu.item :label=\"__('Select all records')\" icon=\"plus\" wire:click=\"selectTable(true)\" x-on:click=\"close\" />\n            <x-livewire-table::dropdown.menu.item :label=\"__('Subtract records')\" icon=\"min\" wire:click=\"selectTable(false)\" x-on:click=\"close\" />\n            <x-livewire-table::dropdown.menu.item :label=\"__('Clear selection')\" icon=\"x-mark\" wire:click=\"clearSelection\" x-on:click=\"close\" />\n        </x-livewire-table::dropdown.menu>\n    </x-livewire-table::dropdown.content>\n</x-livewire-table::dropdown.section>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/dropdowns/sections/trashed.blade.php",
    "content": "<x-livewire-table::dropdown.section section=\"trashed\">\n    <x-livewire-table::dropdown.header :label=\"__('Visibility')\" icon=\"chevron-left\" navigate=\"configuration\" />\n    <x-livewire-table::dropdown.content>\n        <x-livewire-table::form.group :label=\"__('Show')\">\n            <x-livewire-table::form.select name=\"trashed\" wire:model.live=\"trashed\">\n                <option value=\"withoutTrashed\">@lang('Without Trashed')</option>\n                <option value=\"withTrashed\">@lang('With Trashed')</option>\n                <option value=\"onlyTrashed\">@lang('Only Trashed')</option>\n            </x-livewire-table::form.select>\n        </x-livewire-table::form.group>\n        <x-livewire-table::dropdown.footer>\n            @lang('Configure what type of records should be shown in the table.')\n        </x-livewire-table::dropdown.footer>\n    </x-livewire-table::dropdown.content>\n</x-livewire-table::dropdown.section>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/dropdowns/suggestions.blade.php",
    "content": "<x-livewire-table::dropdown current=\"suggestions\">\n    <x-livewire-table::notification.button\n        :title=\"__('Suggestions')\"\n        :aria-label=\"__('Suggestions')\"\n        x-on:click=\"toggle\"\n        class=\"rounded-r-md\"\n    >\n        <x-livewire-table::icon class=\"size-6\" icon=\"ellipsis-vertical\" />\n    </x-livewire-table::notification.button>\n    <x-slot:body>\n        @include('livewire-table::toolbar.dropdowns.sections.suggestions')\n    </x-slot:body>\n</x-livewire-table::dropdown>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/loader.blade.php",
    "content": "<div wire:loading.delay class=\"absolute left-1/2 top-1 z-20 -translate-x-1/2\">\n    <div class=\"bg-base-850 border border-2 border-base-700 border-r-red motion-safe:animate-spin rounded-full p-1.5 transition-all duration-200\"></div>\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/notification.blade.php",
    "content": "<div class=\"flex-1 basis-full order-last lg:order-none lg:basis-auto\">\n    @if($this->isReordering())\n        <x-livewire-table::notification :label=\"__('Drag records to reorder them.')\" icon=\"information-circle\">\n            <x-livewire-table::notification.button\n                :title=\"__('Close')\"\n                :aria-label=\"__('Close')\"\n                wire:click=\"$toggle('reordering')\"\n                class=\"rounded-r-md\"\n            >\n                <x-livewire-table::icon class=\"size-6\" icon=\"x-mark\" />\n            </x-livewire-table::notification.button>\n        </x-livewire-table::notification>\n    @else\n        <template x-if=\"$wire.selected.length > 0\">\n            <x-livewire-table::notification icon=\"check-circle\">\n                <x-slot:label>\n                    <template x-if=\"$wire.selected.length === 1\">\n                        <span>\n                            @lang('Selected 1 record.')\n                        </span>\n                    </template>\n                    <template x-if=\"$wire.selected.length !== 1\">\n                        <span>\n                            @lang('Selected :count records.', ['count' => '<span x-text=\"$wire.selected.length\"></span>'])\n                        </span>\n                    </template>\n                </x-slot:label>\n                @include('livewire-table::toolbar.dropdowns.suggestions')\n            </x-livewire-table::notification>\n        </template>\n    @endif\n</div>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/search.blade.php",
    "content": "<label class=\"relative group/search w-full sm:w-auto\">\n    <x-livewire-table::icon icon=\"magnifying-glass\" class=\"absolute left-3 top-2.5 text-base-400 size-5 transition-colors duration-200 group-hover/search:text-base-300\" />\n    <input\n        type=\"search\"\n        placeholder=\"@lang('Search all columns...')\"\n        @class([\n            'pl-10 pr-4 py-2 rounded-lg border transition-all duration-200 w-full',\n            'focus:outline-none focus:ring-2 focus:ring-red/50 focus:border-red focus:z-10',\n            'bg-base-800 group-hover/search:bg-base-700 active:bg-base-700',\n            'border-base-700 group-hover/search:border-base-600 focus:border-red',\n            'text-base-100 placeholder:text-base-400',\n        ])\n        wire:model.live.debounce.500ms=\"globalSearch\"\n    >\n</label>\n"
  },
  {
    "path": "resources/views/vendor/livewire-table/toolbar/toolbar.blade.php",
    "content": "@php($actions = $this->resolveActions())\n\n<div class=\"bg-base-900 flex items-center flex-wrap gap-2 px-3 py-1.5 sm:px-4 sm:py-2 rounded-t-lg flex-1 border-b border-base-700 transition-all duration-200\">\n    @include('livewire-table::toolbar.loader')\n    @includeWhen($this->canSearch(), 'livewire-table::toolbar.search')\n    @includeWhen($this->canClearSearch(), 'livewire-table::toolbar.buttons.clear-search')\n    @include('livewire-table::toolbar.notification')\n    <div class=\"flex items-center gap-2 ml-auto\">\n        @includeWhen($this->useReordering, 'livewire-table::toolbar.buttons.reordering')\n        @includeWhen($actions->isNotEmpty(), 'livewire-table::toolbar.dropdowns.actions')\n        @include('livewire-table::toolbar.dropdowns.configuration')\n    </div>\n</div>\n"
  },
  {
    "path": "resources/views/vendor/mail/html/button.blade.php",
    "content": "@props([\n    'url',\n    'color' => 'primary',\n    'align' => 'center',\n])\n@php\n    $baseStyle = 'display:inline-block;padding:14px 34px;font-weight:600;font-size:15px;text-decoration:none;border-radius:999px;letter-spacing:0.02em;text-transform:none;border:0;';\n    $palettes = [\n        'primary' => 'color:#FAFAFF;background-color:#EF4444;background-image:linear-gradient(120deg,#EF4444 0%,#F97316 50%,#EF4444 100%);',\n        'success' => 'color:#FAFAFF;background-color:#10B981;background-image:linear-gradient(120deg,#10B981 0%,#34D399 100%);',\n        'error' => 'color:#FAFAFF;background-color:#DC2626;background-image:linear-gradient(120deg,#DC2626 0%,#EF4444 100%);',\n        'secondary' => 'color:#F4F4FA;background-color:transparent;border:1px solid #444459;',\n    ];\n    $buttonStyle = $baseStyle . ($palettes[$color] ?? $palettes['primary']);\n@endphp\n<table class=\"action\" align=\"{{ $align }}\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\">\n<tr>\n<td align=\"{{ $align }}\">\n<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\">\n<tr>\n<td align=\"{{ $align }}\">\n<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\">\n<tr>\n<td>\n<a href=\"{{ $url }}\" class=\"button button-{{ $color }}\" target=\"_blank\" rel=\"noopener\" style=\"{{ $buttonStyle }}\">{{ $slot }}</a>\n</td>\n</tr>\n</table>\n</td>\n</tr>\n</table>\n</td>\n</tr>\n</table>\n"
  },
  {
    "path": "resources/views/vendor/mail/html/footer.blade.php",
    "content": "<tr>\n<td class=\"content-cell\" align=\"center\" style=\"background-color:#000000;color:#ffffff;font-size:13px;padding:32px 16px;line-height:1.6;letter-spacing:0.01em;text-align:center;\">\n{{ Illuminate\\Mail\\Markdown::parse($slot) }}\n</td>\n</tr>\n"
  },
  {
    "path": "resources/views/vendor/mail/html/header.blade.php",
    "content": "@props(['url'])\n<tr>\n<td class=\"header\" style=\"padding:48px 0 16px;text-align:center;background-color:#0A0A0F;\">\n<a href=\"{{ $url }}\" style=\"display:inline-block;text-decoration:none;\">\n<img src=\"https://govigilant.io/img/logo.svg\" class=\"logo\" alt=\"Vigilant Logo\" style=\"display:block;height:64px;width:auto;\">\n</a>\n</td>\n</tr>\n"
  },
  {
    "path": "resources/views/vendor/mail/html/layout.blade.php",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<title>{{ config('app.name') }}</title>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<meta name=\"color-scheme\" content=\"light\">\n<meta name=\"supported-color-schemes\" content=\"light\">\n<style>\n@import url('https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700&display=swap');\n\n:root {\n    color-scheme: dark;\n    supported-color-schemes: dark;\n}\n\nbody {\n    margin: 0;\n    background-color: #0A0A0F;\n    color: #C8C8DC;\n    font-family: 'Figtree', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;\n    -webkit-font-smoothing: antialiased;\n}\n\ntable {\n    border-collapse: collapse;\n}\n\na {\n    color: #60A5FA;\n}\n\n.wrapper {\n    background-color: #0A0A0F;\n    padding: 0 16px 40px;\n    width: 100%;\n}\n\n.content {\n    width: 100%;\n}\n\n.header {\n    padding: 48px 0 12px;\n    background-color: #0A0A0F;\n}\n\n.body {\n    width: 100%;\n    background-color: #0A0A0F;\n}\n\n.inner-body {\n    width: 600px;\n    background-color: #1A1A24;\n    border-radius: 28px;\n    box-shadow: 0 35px 65px rgba(5, 5, 10, 0.65);\n    overflow: hidden;\n}\n\n.content-cell {\n    padding: 48px 48px 40px;\n    color: #C8C8DC;\n    font-size: 16px;\n    line-height: 1.7;\n}\n\n.content-cell h1,\n.content-cell h2,\n.content-cell h3 {\n    color: #FAFAFF;\n    font-weight: 600;\n    margin-top: 0;\n}\n\n.content-cell h1 {\n    font-size: 30px;\n    margin-bottom: 18px;\n}\n\n.content-cell h2 {\n    font-size: 24px;\n    margin-bottom: 14px;\n}\n\n.content-cell h3 {\n    font-size: 20px;\n    margin-bottom: 12px;\n}\n\n.content-cell p,\n.content-cell ul,\n.content-cell ol {\n    margin: 0 0 16px;\n    color: #C8C8DC;\n}\n\n.content-cell strong {\n    color: #F4F4FA;\n}\n\n.content-cell hr {\n    border: 0;\n    border-top: 1px solid #2D2D42;\n    margin: 32px 0;\n}\n\n.panel {\n    margin: 32px 0;\n    border-radius: 20px;\n    border: 1px solid #2D2D42;\n    background-color: #232333;\n}\n\n.panel-content {\n    padding: 0 32px 32px;\n}\n\n.panel-item {\n    color: #C8C8DC;\n}\n\n.table table {\n    width: 100% !important;\n    background-color: #232333;\n    border-radius: 18px;\n    border: 1px solid #2D2D42;\n}\n\n.subcopy {\n    margin-top: 32px;\n    border-radius: 20px;\n    background-color: #232333;\n    border: 1px solid #2D2D42;\n}\n\n.subcopy td {\n    padding: 20px 24px;\n    color: #A8A8C0;\n    font-size: 13px;\n}\n\n.footer {\n    background-color: #000000;\n    border-top: 1px solid #2D2D42;\n}\n\n.footer .content-cell {\n    color: #7A7A94;\n    font-size: 13px;\n    padding: 32px 16px;\n    letter-spacing: 0.01em;\n    text-align: center;\n}\n\n.action {\n    width: 100%;\n    margin: 36px 0;\n}\n\n.button {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    padding: 14px 34px;\n    font-weight: 600;\n    font-size: 15px;\n    text-decoration: none;\n    border-radius: 999px;\n    color: #FAFAFF !important;\n    background-color: #EF4444;\n    background-image: linear-gradient(120deg, #EF4444 0%, #F97316 50%, #EF4444 100%);\n    border: 0;\n    letter-spacing: 0.02em;\n}\n\n.button-secondary {\n    background-color: transparent;\n    background-image: none;\n    color: #F4F4FA !important;\n    border: 1px solid #444459;\n    box-shadow: none;\n}\n\n.button-success {\n    background-color: #10B981;\n    background-image: linear-gradient(120deg, #10B981 0%, #34D399 100%);\n}\n\n.button-error {\n    background-color: #DC2626;\n    background-image: linear-gradient(120deg, #DC2626 0%, #EF4444 100%);\n}\n\n@media only screen and (max-width: 600px) {\n.inner-body {\nwidth: 100% !important;\n}\n\n.footer {\nwidth: 100% !important;\n}\n\n.content-cell {\npadding: 32px 24px;\n}\n}\n\n@media only screen and (max-width: 500px) {\n.button {\nwidth: 100% !important;\n}\n}\n</style>\n</head>\n<body style=\"margin:0;padding:0;background-color:#0A0A0F;color:#C8C8DC;\">\n\n<table class=\"wrapper\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background-color:#0A0A0F;\">\n<tr>\n<td align=\"center\" style=\"background-color:#0A0A0F;\">\n<table class=\"content\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;background-color:#0A0A0F;\">\n{{ $header ?? '' }}\n\n<!-- Email Body -->\n<tr>\n<td class=\"body\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"border: hidden !important;background-color:#0A0A0F;padding:24px 0 48px;\">\n<table class=\"inner-body\" align=\"center\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background-color:#1A1A24;border-radius:28px;box-shadow:0 35px 65px rgba(5,5,10,0.65);overflow:hidden;\">\n<!-- Body content -->\n<tr>\n<td class=\"content-cell\" style=\"padding:48px 48px 40px;color:#C8C8DC;font-size:16px;line-height:1.7;\">\n{{ Illuminate\\Mail\\Markdown::parse($slot) }}\n\n{{ $subcopy ?? '' }}\n</td>\n</tr>\n</table>\n</td>\n</tr>\n\n{{ $footer ?? '' }}\n</table>\n</td>\n</tr>\n</table>\n</body>\n</html>\n"
  },
  {
    "path": "resources/views/vendor/mail/html/message.blade.php",
    "content": "<x-mail::layout>\n{{-- Header --}}\n<x-slot:header>\n<x-mail::header :url=\"config('app.url')\"></x-mail::header>\n</x-slot:header>\n\n{{-- Body --}}\n{{ $slot }}\n\n{{-- Subcopy --}}\n@isset($subcopy)\n<x-slot:subcopy>\n<x-mail::subcopy>\n{{ $subcopy }}\n</x-mail::subcopy>\n</x-slot:subcopy>\n@endisset\n\n{{-- Footer --}}\n<x-slot:footer>\n<x-mail::footer>\n<p style=\"margin: 0; background-color: #000000; padding: 16px; text-align: center; color: #ffffff;\">\n© {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights reserved.') }}\n</p>\n</x-mail::footer>\n</x-slot:footer>\n</x-mail::layout>\n"
  },
  {
    "path": "resources/views/vendor/mail/html/panel.blade.php",
    "content": "<table class=\"panel\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"margin:32px 0;border-radius:24px;border:1px solid #3A3A56;background-color:#151524;box-shadow:0 18px 40px rgba(3,3,15,.55);overflow:hidden;\">\n<tr>\n<td style=\"height:8px;background:linear-gradient(120deg,#6C63FF,#61E1FF,#4ECDC4);padding:0;\"></td>\n</tr>\n<tr>\n<td class=\"panel-content\" style=\"padding:32px;background:linear-gradient(180deg,rgba(34,34,58,0.98),rgba(16,16,34,0.98));\">\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\">\n<tr>\n<td class=\"panel-item\" style=\"color:#F4F4FF;font-size:15px;line-height:1.7;\">\n{{ Illuminate\\Mail\\Markdown::parse($slot) }}\n</td>\n</tr>\n</table>\n</td>\n</tr>\n</table>\n\n"
  },
  {
    "path": "resources/views/vendor/mail/html/subcopy.blade.php",
    "content": "<table class=\"subcopy\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"margin-top:32px;border-radius:20px;background-color:#232333;border:1px solid #2D2D42;\">\n<tr>\n<td style=\"padding:20px 24px;color:#A8A8C0;font-size:13px;line-height:1.6;\">\n{{ Illuminate\\Mail\\Markdown::parse($slot) }}\n</td>\n</tr>\n</table>\n"
  },
  {
    "path": "resources/views/vendor/mail/html/table.blade.php",
    "content": "<div class=\"table\" style=\"margin:32px 0;border-radius:18px;border:1px solid #2D2D42;background-color:#232333;overflow:hidden;\">\n{{ Illuminate\\Mail\\Markdown::parse($slot) }}\n</div>\n"
  },
  {
    "path": "resources/views/vendor/mail/html/themes/default.css",
    "content": "/* Base */\n\nbody,\nbody *:not(html):not(style):not(br):not(tr):not(code) {\n    box-sizing: border-box;\n    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,\n        'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';\n    position: relative;\n}\n\nbody {\n    -webkit-text-size-adjust: none;\n    background-color: #100F0F;\n    color: #E6E4D9;\n    height: 100%;\n    line-height: 1.4;\n    margin: 0;\n    padding: 0;\n    width: 100% !important;\n}\n\np,\nul,\nol,\nblockquote {\n    line-height: 1.4;\n    text-align: left;\n}\n\na {\n    color: #3869d4;\n}\n\na img {\n    border: none;\n}\n\n/* Typography */\n\nh1 {\n    color: #FFFCF0;\n    font-size: 18px;\n    font-weight: bold;\n    margin-top: 0;\n    text-align: left;\n}\n\nh2 {\n    font-size: 16px;\n    font-weight: bold;\n    margin-top: 0;\n    text-align: left;\n}\n\nh3 {\n    font-size: 14px;\n    font-weight: bold;\n    margin-top: 0;\n    text-align: left;\n}\n\np {\n    font-size: 16px;\n    line-height: 1.5em;\n    margin-top: 0;\n    text-align: left;\n}\n\np.sub {\n    font-size: 12px;\n}\n\nimg {\n    max-width: 100%;\n}\n\n/* Layout */\n\n.wrapper {\n    -premailer-cellpadding: 0;\n    -premailer-cellspacing: 0;\n    -premailer-width: 100%;\n    background-color: #edf2f7;\n    margin: 0;\n    padding: 0;\n    width: 100%;\n}\n\n.content {\n    -premailer-cellpadding: 0;\n    -premailer-cellspacing: 0;\n    -premailer-width: 100%;\n    margin: 0;\n    padding: 0;\n    width: 100%;\n    background-color: #100F0F;\n}\n\n/* Header */\n\n.header {\n    padding: 25px 0;\n    text-align: center;\n}\n\n.header a {\n    color: #3d4852;\n    font-size: 19px;\n    font-weight: bold;\n    text-decoration: none;\n}\n\n/* Logo */\n\n.logo {\n    height: 75px;\n    max-height: 75px;\n    width: 200px;\n}\n\n/* Body */\n\n.body {\n    -premailer-cellpadding: 0;\n    -premailer-cellspacing: 0;\n    -premailer-width: 100%;\n    background-color: #100F0F;\n    border-bottom: 1px solid #edf2f7;\n    border-top: 1px solid #edf2f7;\n    margin: 0;\n    padding: 0;\n    width: 100%;\n}\n\n.inner-body {\n    -premailer-cellpadding: 0;\n    -premailer-cellspacing: 0;\n    -premailer-width: 570px;\n    background-color: #ffffff;\n    border-color: #e8e5ef;\n    border-radius: 2px;\n    border-width: 1px;\n    box-shadow: 0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015);\n    margin: 0 auto;\n    padding: 0;\n    width: 570px;\n}\n\n/* Subcopy */\n\n.subcopy {\n    border-top: 1px solid #e8e5ef;\n    margin-top: 25px;\n    padding-top: 25px;\n}\n\n.subcopy p {\n    font-size: 14px;\n}\n\n/* Footer */\n\n.footer {\n    -premailer-cellpadding: 0;\n    -premailer-cellspacing: 0;\n    -premailer-width: 570px;\n    margin: 0 auto;\n    padding: 0;\n    text-align: center;\n    width: 570px;\n}\n\n.footer p {\n    color: #b0adc5;\n    font-size: 12px;\n    text-align: center;\n}\n\n.footer a {\n    color: #b0adc5;\n    text-decoration: underline;\n}\n\n/* Tables */\n\n.table table {\n    -premailer-cellpadding: 0;\n    -premailer-cellspacing: 0;\n    -premailer-width: 100%;\n    margin: 30px auto;\n    width: 100%;\n}\n\n.table th {\n    border-bottom: 1px solid #edeff2;\n    margin: 0;\n    padding-bottom: 8px;\n}\n\n.table td {\n    color: #74787e;\n    font-size: 15px;\n    line-height: 18px;\n    margin: 0;\n    padding: 10px 0;\n}\n\n.content-cell {\n    max-width: 100vw;\n    padding: 32px;\n    background-color: #282726;\n}\n\n/* Buttons */\n\n.action {\n    -premailer-cellpadding: 0;\n    -premailer-cellspacing: 0;\n    -premailer-width: 100%;\n    margin: 30px auto;\n    padding: 0;\n    text-align: center;\n    width: 100%;\n    float: unset;\n}\n\n.button {\n    -webkit-text-size-adjust: none;\n    border-radius: 4px;\n    color: #fff;\n    display: inline-block;\n    overflow: hidden;\n    text-decoration: none;\n}\n\n.button-blue,\n.button-primary {\n    background-color: #AF3029;\n    border-bottom: 8px solid #AF3029;\n    border-left: 18px solid #AF3029;\n    border-right: 18px solid #AF3029;\n    border-top: 8px solid #AF3029;\n}\n\n.button-green,\n.button-success {\n    background-color: #48bb78;\n    border-bottom: 8px solid #48bb78;\n    border-left: 18px solid #48bb78;\n    border-right: 18px solid #48bb78;\n    border-top: 8px solid #48bb78;\n}\n\n.button-red,\n.button-error {\n    background-color: #e53e3e;\n    border-bottom: 8px solid #e53e3e;\n    border-left: 18px solid #e53e3e;\n    border-right: 18px solid #e53e3e;\n    border-top: 8px solid #e53e3e;\n}\n\n/* Panels */\n\n.panel {\n    border-left: #2d3748 solid 4px;\n    margin: 21px 0;\n}\n\n.panel-content {\n    background-color: #282726;\n    color: #E6E4D9;\n    padding: 16px;\n}\n\n.panel-content p {\n    color: #E6E4D9;\n}\n\n.panel-item {\n    padding: 0;\n}\n\n.panel-item p:last-of-type {\n    margin-bottom: 0;\n    padding-bottom: 0;\n}\n\n/* Utilities */\n\n.break-all {\n    word-break: break-all;\n}\n"
  },
  {
    "path": "resources/views/vendor/mail/text/button.blade.php",
    "content": "{{ $slot }}: {{ $url }}\n"
  },
  {
    "path": "resources/views/vendor/mail/text/footer.blade.php",
    "content": "{{ $slot }}\n"
  },
  {
    "path": "resources/views/vendor/mail/text/header.blade.php",
    "content": "{{ $slot }}: {{ $url }}\n"
  },
  {
    "path": "resources/views/vendor/mail/text/layout.blade.php",
    "content": "{!! strip_tags($header ?? '') !!}\n\n{!! strip_tags($slot) !!}\n@isset($subcopy)\n\n{!! strip_tags($subcopy) !!}\n@endisset\n\n{!! strip_tags($footer ?? '') !!}\n"
  },
  {
    "path": "resources/views/vendor/mail/text/message.blade.php",
    "content": "<x-mail::layout>\n    {{-- Header --}}\n    <x-slot:header>\n        <x-mail::header :url=\"config('app.url')\">\n            {{ config('app.name') }}\n        </x-mail::header>\n    </x-slot:header>\n\n    {{-- Body --}}\n    {{ $slot }}\n\n    {{-- Subcopy --}}\n    @isset($subcopy)\n        <x-slot:subcopy>\n            <x-mail::subcopy>\n                {{ $subcopy }}\n            </x-mail::subcopy>\n        </x-slot:subcopy>\n    @endisset\n\n    {{-- Footer --}}\n    <x-slot:footer>\n        <x-mail::footer>\n            © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')\n        </x-mail::footer>\n    </x-slot:footer>\n</x-mail::layout>\n"
  },
  {
    "path": "resources/views/vendor/mail/text/panel.blade.php",
    "content": "{{ $slot }}\n"
  },
  {
    "path": "resources/views/vendor/mail/text/subcopy.blade.php",
    "content": "{{ $slot }}\n"
  },
  {
    "path": "resources/views/vendor/mail/text/table.blade.php",
    "content": "{{ $slot }}\n"
  },
  {
    "path": "routes/api.php",
    "content": "<?php\n\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Route;\n\n/*\n|--------------------------------------------------------------------------\n| API Routes\n|--------------------------------------------------------------------------\n|\n| Here is where you can register API routes for your application. These\n| routes are loaded by the RouteServiceProvider and all of them will\n| be assigned to the \"api\" middleware group. Make something great!\n|\n*/\n\nRoute::middleware('auth:sanctum')->get('/user', function (Request $request) {\n    return $request->user();\n});\n"
  },
  {
    "path": "routes/channels.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Broadcast;\n\n/*\n|--------------------------------------------------------------------------\n| Broadcast Channels\n|--------------------------------------------------------------------------\n|\n| Here you may register all of the event broadcasting channels that your\n| application supports. The given channel authorization callbacks are\n| used to check if an authenticated user can listen to the channel.\n|\n*/\n\nBroadcast::channel('App.Models.User.{id}', function ($user, $id) {\n    return (int) $user->id === (int) $id;\n});\n"
  },
  {
    "path": "routes/console.php",
    "content": "<?php\n\nuse Illuminate\\Foundation\\Inspiring;\nuse Illuminate\\Support\\Facades\\Artisan;\n\n/*\n|--------------------------------------------------------------------------\n| Console Routes\n|--------------------------------------------------------------------------\n|\n| This file is where you may define all of your Closure based console\n| commands. Each Closure is bound to a command instance allowing a\n| simple approach to interacting with each command's IO methods.\n|\n*/\n\nArtisan::command('inspire', function () {\n    $this->comment(Inspiring::quote());\n})->purpose('Display an inspiring quote');\n"
  },
  {
    "path": "routes/web.php",
    "content": "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\n\n/*\n|--------------------------------------------------------------------------\n| Web Routes\n|--------------------------------------------------------------------------\n|\n| Here is where you can register web routes for your application. These\n| routes are loaded by the RouteServiceProvider and all of them will\n| be assigned to the \"web\" middleware group. Make something great!\n|\n*/\n\nRoute::middleware([\n    'auth:sanctum',\n    config('jetstream.auth_session'),\n    'verified',\n])->group(function () {\n    Route::get('/', fn () => response()->redirectToRoute('sites'));\n    Route::fallback(fn () => abort(404));\n});\n"
  },
  {
    "path": "scripts/generate-tailwind-sources.mjs",
    "content": "import { promises as fs } from 'node:fs';\nimport path from 'node:path';\n\nconst projectRoot = path.resolve(process.cwd());\nconst cssDir = path.join(projectRoot, 'resources', 'css');\nconst packagesDir = path.join(projectRoot, 'packages');\nconst outputFile = path.join(cssDir, 'tailwind.sources.css');\n\nconst STATIC_SOURCES = [\n  \"../views/**/*.blade.php\",\n  \"../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php\",\n  \"../../vendor/laravel/jetstream/resources/views/**/*.blade.php\",\n  \"../../vendor/laravel/jetstream/stubs/livewire/resources/views/**/*.blade.php\",\n  \"../../vendor/ramonrietdijk/livewire-tables/resources/views/**/*.blade.php\",\n];\n\nconst IGNORED_DIRS = new Set(['.', '..', 'vendor', 'node_modules']);\n\nasync function directoryExists(targetPath) {\n  try {\n    const stat = await fs.stat(targetPath);\n    return stat.isDirectory();\n  } catch {\n    return false;\n  }\n}\n\nasync function collectPackageViewsSources() {\n  const sources = [];\n\n  if (!await directoryExists(packagesDir)) {\n    return sources;\n  }\n\n  const entries = await fs.readdir(packagesDir, { withFileTypes: true }).catch(() => []);\n\n  for (const entry of entries) {\n    if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;\n    if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;\n\n    const pkgPath = path.join(packagesDir, entry.name);\n\n    // Check for direct views: packages/*/views\n    const directViewsDir = path.join(pkgPath, 'views');\n    if (await directoryExists(directViewsDir)) {\n      const relativePath = path.relative(cssDir, directViewsDir).replace(/\\\\/g, '/');\n      sources.push(`${relativePath}/**/*.blade.php`);\n    }\n\n    // Check for resources/views: packages/*/resources/views\n    const resourcesViewsDir = path.join(pkgPath, 'resources', 'views');\n    if (await directoryExists(resourcesViewsDir)) {\n      const relativePath = path.relative(cssDir, resourcesViewsDir).replace(/\\\\/g, '/');\n      sources.push(`${relativePath}/**/*.blade.php`);\n    }\n\n    // Check for nested packages: packages/saas/*/views\n    const nestedPackagesDir = path.join(pkgPath, 'packages');\n    if (await directoryExists(nestedPackagesDir)) {\n      const nestedEntries = await fs.readdir(nestedPackagesDir, { withFileTypes: true }).catch(() => []);\n      for (const nestedEntry of nestedEntries) {\n        if (!nestedEntry.isDirectory() && !nestedEntry.isSymbolicLink()) continue;\n        if (IGNORED_DIRS.has(nestedEntry.name) || nestedEntry.name.startsWith('.')) continue;\n\n        const nestedPkgPath = path.join(nestedPackagesDir, nestedEntry.name);\n\n        // Check for direct views in nested package\n        const nestedDirectViewsDir = path.join(nestedPkgPath, 'views');\n        if (await directoryExists(nestedDirectViewsDir)) {\n          const relativePath = path.relative(cssDir, nestedDirectViewsDir).replace(/\\\\/g, '/');\n          sources.push(`${relativePath}/**/*.blade.php`);\n        }\n\n        // Check for resources/views in nested package\n        const nestedResourcesViewsDir = path.join(nestedPkgPath, 'resources', 'views');\n        if (await directoryExists(nestedResourcesViewsDir)) {\n          const relativePath = path.relative(cssDir, nestedResourcesViewsDir).replace(/\\\\/g, '/');\n          sources.push(`${relativePath}/**/*.blade.php`);\n        }\n      }\n    }\n  }\n\n  return sources;\n}\n\nasync function collectAllSources() {\n  const dynamicSources = new Set();\n\n  const packageSources = await collectPackageViewsSources();\n  packageSources.forEach((source) => dynamicSources.add(source));\n\n  STATIC_SOURCES.forEach((source) => dynamicSources.add(source));\n  return Array.from(dynamicSources).sort((a, b) => a.localeCompare(b));\n}\n\nfunction buildFileContent(sources) {\n  const header = [\n    '/*',\n    ' * This file is auto-generated by scripts/generate-tailwind-sources.mjs.',\n    ' * Do not edit this file manually.',\n    ' */',\n    '',\n  ].join('\\n');\n\n  const lines = sources.map((pattern) => `@source '${pattern}';`);\n  return `${header}${lines.join('\\n')}\\n`;\n}\n\nasync function main() {\n  const sources = await collectAllSources();\n  const fileContent = buildFileContent(sources);\n  await fs.writeFile(outputFile, fileContent, 'utf8');\n}\n\nmain().catch((error) => {\n  console.error('Failed to generate Tailwind sources:', error);\n  process.exitCode = 1;\n});\n"
  },
  {
    "path": "scripts/package-quality.sh",
    "content": "#!/bin/sh\n\nif [ -n \"$1\" ]; then\n    dirs=\"./packages/$1\"\nelse\n    dirs=\"./packages/*\"\nfi\n\nfor dir in $dirs\ndo\n    echo 'Checking ' $dir\n\n    [ -f \"$dir/composer.lock\" ] && rm \"$dir/composer.lock\"\n\n    composer install --working-dir=$dir --quiet || exit 1\n    composer quality --working-dir=$dir || exit 1\n\n    rm -rf \"$dir/vendor\"\n    rm \"$dir/composer.lock\"\ndone\n"
  },
  {
    "path": "storage/app/.gitignore",
    "content": "*\n!public/\n!.gitignore\n"
  },
  {
    "path": "storage/framework/.gitignore",
    "content": "compiled.php\nconfig.php\ndown\nevents.scanned.php\nmaintenance.php\nroutes.php\nroutes.scanned.php\nschedule-*\nservices.json\n"
  },
  {
    "path": "storage/framework/cache/.gitignore",
    "content": "*\n!data/\n!.gitignore\n"
  },
  {
    "path": "storage/framework/sessions/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "storage/framework/testing/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "storage/framework/views/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "storage/logs/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "tests/Browser/Notifications/ChannelsFormTest.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Notifications;\n\nuse Laravel\\Dusk\\Browser;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Tests\\DuskTestCase;\nuse Vigilant\\Notifications\\Channels\\NtfyChannel;\nuse Vigilant\\Notifications\\Models\\Channel;\n\nclass ChannelsFormTest extends DuskTestCase\n{\n    #[Test]\n    public function it_can_add_channel(): void\n    {\n        $this->browse(function (Browser $browser) {\n            $browser->login()\n                ->visit(route('notifications.channels'))\n                ->click('@channel-add-button')\n                ->assertSee('Choose the notification channel') // Help text of channel dropdown\n                ->select('#form\\.channel', NtfyChannel::class)\n                ->pause(250)\n                ->type('#settings\\.server', 'https://ntfy.govigilant.io')\n                ->type('#settings\\.topic', 'topic')\n                ->pause(1000)\n                ->click('@submit-button')\n                ->pause(250);\n\n            /** @var ?Channel $channel */\n            $channel = Channel::query()->first();\n            $this->assertNotNull($channel);\n            $this->assertEquals(NtfyChannel::class, $channel->channel);\n            $this->assertEquals(['server' => 'https://ntfy.govigilant.io', 'topic' => 'topic'], $channel->settings);\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Notifications/ChannelsIndexTest.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Notifications;\n\nuse Laravel\\Dusk\\Browser;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Tests\\DuskTestCase;\nuse Vigilant\\Notifications\\Channels\\NtfyChannel;\nuse Vigilant\\Notifications\\Models\\Channel;\n\nclass ChannelsIndexTest extends DuskTestCase\n{\n    #[Test]\n    public function it_shows_table(): void\n    {\n        $this->browse(function (Browser $browser) {\n\n            $this->user();\n\n            Channel::query()->create([\n                'channel' => NtfyChannel::class,\n                'settings' => [],\n            ]);\n\n            $browser->login()\n                ->visit(route('notifications.channels'))\n                ->assertSee('Ntfy');\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Notifications/NotificationsFormTest.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Notifications;\n\nuse Laravel\\Dusk\\Browser;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Tests\\DuskTestCase;\nuse Vigilant\\Uptime\\Notifications\\DowntimeStartNotification;\n\nclass NotificationsFormTest extends DuskTestCase\n{\n    #[Test]\n    public function it_can_add_channel(): void\n    {\n        $this->browse(function (Browser $browser) {\n            $browser->login()\n                ->visit(route('notifications'))\n                ->click('@trigger-add-button')\n                ->assertSee('Choose the event that triggers this notification') // Help text of the trigger dropdown\n                ->type('#form\\.name', 'Test Notification')\n                ->select('#form\\.notification', DowntimeStartNotification::class)\n                ->check('#form\\.all_channels')\n                ->clickAndWaitForReload('@submit-button')\n                ->assertPathContains('notifications/edit')\n                ->assertSee('Site downtime detected');\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Notifications/NotificationsIndexTest.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Notifications;\n\nuse Laravel\\Dusk\\Browser;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Tests\\DuskTestCase;\nuse Vigilant\\Notifications\\Models\\Trigger;\nuse Vigilant\\Uptime\\Notifications\\DowntimeStartNotification;\n\nclass NotificationsIndexTest extends DuskTestCase\n{\n    #[Test]\n    public function it_shows_table(): void\n    {\n        $this->browse(function (Browser $browser) {\n\n            $this->user();\n\n            Trigger::query()->create([\n                'enabled' => true,\n                'name' => 'Downtime',\n                'notification' => DowntimeStartNotification::class,\n                'conditions' => [],\n            ]);\n\n            $browser->login()\n                ->visit(route('notifications'))\n                ->waitForText(DowntimeStartNotification::$name, 5);\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Pages/HomePage.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Pages;\n\nuse Laravel\\Dusk\\Browser;\n\nclass HomePage extends Page\n{\n    /**\n     * Get the URL for the page.\n     */\n    public function url(): string\n    {\n        return '/';\n    }\n\n    /**\n     * Assert that the browser is on the page.\n     */\n    public function assert(Browser $browser): void\n    {\n        //\n    }\n\n    /**\n     * Get the element shortcuts for the page.\n     *\n     * @return array<string, string>\n     */\n    public function elements(): array\n    {\n        return [\n            '@element' => '#selector',\n        ];\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Pages/Page.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Pages;\n\nuse Laravel\\Dusk\\Page as BasePage;\n\nabstract class Page extends BasePage\n{\n    /**\n     * Get the global element shortcuts for the site.\n     *\n     * @return array<string, string>\n     */\n    public static function siteElements(): array\n    {\n        return [\n            '@element' => '#selector',\n        ];\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Sites/SitesFormTest.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Sites;\n\nuse Laravel\\Dusk\\Browser;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Tests\\DuskTestCase;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass SitesFormTest extends DuskTestCase\n{\n    #[Test]\n    public function it_can_add_site(): void\n    {\n        $this->browse(function (Browser $browser) {\n            $browser->login()\n                ->visit(route('sites'))\n                ->click('@site-add-button')\n                ->assertSee('The URL of the site that you want to add.') // Help text of URL field\n                ->click('@submit-button')\n                ->waitForText('field is required')\n                ->assertSee('field is required')\n                ->type('#form\\.url', 'invalid value')\n                ->click('@submit-button')\n                ->waitForText('must be a valid URL', 5)\n                ->assertSee('must be a valid URL')\n                ->type('#form\\.url', 'https://govigilant.io')\n                ->click('@submit-button')\n                ->pause(250);\n\n            /** @var ?Site $createdSite */\n            $createdSite = Site::query()->firstWhere('url', '=', 'https://govigilant.io');\n            $this->assertNotNull($createdSite);\n        });\n    }\n\n    #[Test]\n    public function it_can_add_uptime_monitor_via_tab(): void\n    {\n        $this->browse(function (Browser $browser) {\n            $this->user();\n\n            /** @var Site $site */\n            $site = Site::query()->create([\n                'url' => 'https://govigilant.io',\n            ]);\n\n            $browser->login()\n                ->visit(route('site.edit', ['site' => $site]))\n                ->waitFor('@uptime-tab-enabled')\n                ->check('@uptime-tab-enabled')\n                ->waitForText('Friendly name for this monitor')\n                ->click('@submit-button')\n                ->pause(250)\n                ->visit(route('uptime'))\n                ->pause(250)\n                ->assertSee('https://govigilant.io');\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Sites/SitesIndexTest.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Sites;\n\nuse Laravel\\Dusk\\Browser;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Tests\\DuskTestCase;\nuse Vigilant\\Sites\\Models\\Site;\n\nclass SitesIndexTest extends DuskTestCase\n{\n    #[Test]\n    public function it_shows_table(): void\n    {\n        $this->browse(function (Browser $browser) {\n\n            $this->user();\n\n            Site::query()->create([\n                'team_id' => 1,\n                'url' => 'https://govigilant.io',\n            ]);\n\n            $browser->login()\n                ->visit(route('sites'))\n                ->assertSee('https://govigilant.io');\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Uptime/UptimeFormTest.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Uptime;\n\nuse Laravel\\Dusk\\Browser;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Tests\\DuskTestCase;\n\nclass UptimeFormTest extends DuskTestCase\n{\n    #[Test]\n    public function it_can_add_monitor(): void\n    {\n        $this->browse(function (Browser $browser) {\n            $browser->login()\n                ->visit(route('uptime'))\n                ->click('@monitor-add-button')\n                ->assertSee('Friendly name for this monitor') // Help text of name field\n                ->type('#form\\.name', 'Test Monitor')\n                ->select('#form\\.type', 'ping')\n                ->pause(500)\n                ->type('#form\\.settings\\.host', 'govigilant.io')\n                ->type('#form\\.settings\\.port', 22)\n                ->select('#form\\.interval', '* * * * */2')\n                ->type('#form\\.retries', 5)\n                ->type('#form\\.timeout', 10)\n                ->clickAndWaitForReload('@submit-button')\n                ->assertUrlIs(route('uptime'))\n                ->assertSee('Test Monitor');\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Uptime/UptimeIndexTest.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Uptime;\n\nuse Laravel\\Dusk\\Browser;\nuse PHPUnit\\Framework\\Attributes\\Test;\nuse Tests\\DuskTestCase;\nuse Vigilant\\Uptime\\Enums\\Type;\nuse Vigilant\\Uptime\\Models\\Monitor;\n\nclass UptimeIndexTest extends DuskTestCase\n{\n    #[Test]\n    public function it_shows_table(): void\n    {\n        $this->browse(function (Browser $browser) {\n\n            $this->user();\n\n            Monitor::query()->create([\n                'name' => 'Test Monitor',\n                'type' => Type::Http,\n                'settings' => [\n                    'host' => 'https://govigilant.io',\n                ],\n                'interval' => '* * * * *',\n                'timeout' => 60,\n                'retries' => 3,\n            ]);\n\n            $browser->login()\n                ->visit(route('uptime'))\n                ->assertSee('Test Monitor');\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Browser/console/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "tests/Browser/screenshots/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "tests/Browser/source/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "tests/CreatesApplication.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse Illuminate\\Contracts\\Console\\Kernel;\nuse Illuminate\\Foundation\\Application;\n\ntrait CreatesApplication\n{\n    /**\n     * Creates the application.\n     */\n    public function createApplication(): Application\n    {\n        $app = require __DIR__.'/../bootstrap/app.php';\n\n        $app->make(Kernel::class)->bootstrap();\n\n        return $app;\n    }\n}\n"
  },
  {
    "path": "tests/DuskTestCase.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse Facebook\\WebDriver\\Chrome\\ChromeOptions;\nuse Facebook\\WebDriver\\Remote\\DesiredCapabilities;\nuse Facebook\\WebDriver\\Remote\\RemoteWebDriver;\nuse Illuminate\\Foundation\\Testing\\DatabaseMigrations;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse Laravel\\Dusk\\TestCase as BaseTestCase;\nuse PHPUnit\\Framework\\Attributes\\BeforeClass;\nuse Vigilant\\Users\\Actions\\Jetstream\\CreateTeam;\nuse Vigilant\\Users\\Models\\User;\n\nabstract class DuskTestCase extends BaseTestCase\n{\n    use CreatesApplication;\n    use DatabaseMigrations;\n\n    /**\n     * Prepare for Dusk test execution.\n     */\n    #[BeforeClass]\n    public static function prepare(): void\n    {\n        if (! static::runningInSail()) {\n            static::startChromeDriver();\n        }\n    }\n\n    /**\n     * Create the RemoteWebDriver instance.\n     */\n    protected function driver(): RemoteWebDriver\n    {\n        $options = (new ChromeOptions)->addArguments(collect([\n            $this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080',\n        ])->unless($this->hasHeadlessDisabled(), function (Collection $items) {\n            return $items->merge([\n                '--disable-gpu',\n                '--headless=new',\n            ]);\n        })->all());\n\n        return RemoteWebDriver::create(\n            $_ENV['DUSK_DRIVER_URL'] ?? 'http://localhost:9515',\n            DesiredCapabilities::chrome()->setCapability(\n                ChromeOptions::CAPABILITY, $options\n            )\n        );\n    }\n\n    /**\n     * Determine whether the Dusk command has disabled headless mode.\n     */\n    protected function hasHeadlessDisabled(): bool\n    {\n        return isset($_SERVER['DUSK_HEADLESS_DISABLED']) ||\n            isset($_ENV['DUSK_HEADLESS_DISABLED']);\n    }\n\n    /**\n     * Determine if the browser window should start maximized.\n     */\n    protected function shouldStartMaximized(): bool\n    {\n        return isset($_SERVER['DUSK_START_MAXIMIZED']) ||\n            isset($_ENV['DUSK_START_MAXIMIZED']);\n    }\n\n    protected function user(): User\n    {\n        /** @var User $user */\n        $user = User::query()->firstOrCreate([\n            'email' => 'tester@govigilant.io',\n        ], [\n            'name' => 'Tester',\n            'password' => bcrypt('password'),\n            'current_team_id' => 1,\n        ]);\n\n        if ($user->currentTeam === null) {\n            /** @var CreateTeam $createTeam */\n            $createTeam = app(CreateTeam::class);\n\n            $team = $createTeam->create($user, [\n                'name' => 'Tester\\'s Team',\n            ]);\n\n            $team->users()->attach($user);\n        }\n\n        Auth::login($user);\n\n        return $user;\n    }\n}\n"
  },
  {
    "path": "tests/Feature/.gitkeep",
    "content": ""
  },
  {
    "path": "tests/TestCase.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse Illuminate\\Foundation\\Testing\\TestCase as BaseTestCase;\n\nabstract class TestCase extends BaseTestCase\n{\n    use CreatesApplication;\n}\n"
  },
  {
    "path": "tests/Unit/.gitkeep",
    "content": ""
  },
  {
    "path": "vite.config.js",
    "content": "import { defineConfig } from 'vite';\nimport laravel from 'laravel-vite-plugin';\n\nexport default defineConfig({\n    plugins: [\n        laravel({\n            input: [\n                'resources/css/app.css',\n                'resources/js/app.js',\n            ],\n            refresh: true,\n        }),\n    ],\n    server: {\n        watch: {\n            ignored: [\n                '**/vendor/**',\n                '**/packages/**',\n            ],\n        },\n    },\n});\n"
  }
]