feat: add FrankenPHP and Laravel Octane (#2219)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added optional Octane worker mode, updated Docker runtime, and a CI
smoke-test workflow for container validation.

* **Documentation**
* Added production deployment and Octane guidance, plus notes for module
authors about request-safe state.

* **Bug Fixes**
* Ensure activity logging resets per request, deterministic theme
selection, and stable module link handling across requests.

* **Tests**
* Added Octane-focused tests for logging reset, theme isolation, and
module link stability.

* **Chores**
* Removed obsolete local dev/example scripts, compose/make/just helpers,
and related example configs.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/phpvms/phpvms/pull/2219?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Nabeel S. 2026-05-28 14:00:25 -05:00 committed by GitHub
commit e7c3cc017e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 1753 additions and 944 deletions

148
.dockerignore Normal file
View File

@ -0,0 +1,148 @@
# .dockerignore
# Generated by configs.sh
# Secrets & sensitive files (ALWAYS EXCLUDED FOR SECURITY)
.env
.env.*
*.key
*.pem
*_rsa
*_rsa.*
*.p12
*.pfx
.aws/
.ssh/
credentials
secrets.yml
secrets.yaml
# Laravel/PHP
.phpunit.cache
bootstrap/cache/*
!bootstrap/cache/.gitignore
public/build
storage/framework/cache/data/*
storage/framework/sessions/*
storage/framework/views/*
storage/logs/*
storage/*.key
.phpstorm.meta.php
.phpunit.result.cache
.editorconfig
.htaccess
_ide_helper.php
boost.json
phpstan.neon
phpunit.xml
pint.json
railpack.json
rector.php
version.json
# Node.js
node_modules/
.npm/
.yarn/
.bun/
.pnp.*
dist/
build/
.next/
.nuxt/
.cache/
.turbo/
out/
.oxfmtrc.json
.oxlintrc.json
tsconfig.json
vitest.config.ts
### Docker
.docker
Dockerfile
compose.yaml
compose.*
# Version control
.git/
.gitignore
.gitattributes
.gitmodules
.svn/
.hg/
# IDE & Editor files
.vscode/
.idea/
*.swp
*.swo
*~
.project
.classpath
.settings/
# Documentation
*.md
README*
LICENSE*
CHANGELOG*
docs/
documentation/
.readthedocs.yml
mkdocs.yml
# Misc
.agents
.lean-ctx
.opencode
.rtk
.serena
.superpowers
docs/superpowers
openspec
CHANGELOG.md
# Tests & Coverage
test/
tests/
__tests__/
*.test.js
*.test.ts
*.spec.js
*.spec.ts
*.test.py
*_test.go
coverage/
.coverage
htmlcov/
.pytest_cache/
.tox/
jest.config.js
vitest.config.js
# CI/CD
.github/
.gitlab-ci.yml
.circleci/
.travis.yml
azure-pipelines.yml
Jenkinsfile
.drone.yml
# Log files
*.log
logs/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# OS-specific files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
desktop.ini

View File

@ -49,6 +49,14 @@ APP_FALLBACK_LOCALE=en
# Locale used by Faker when generating fake data.
APP_FAKER_LOCALE=en_US
# =============================================================================
# OCTANE AND PRODUCTION CONFIGURATION
# =============================================================================
# If you're using the docker-compose or Railpack to serve directly
# to the internet, instead of a server in front of it, set this to true
OCTANE_HTTPS=false
# =============================================================================
# DATABASE
# =============================================================================

View File

@ -31,11 +31,7 @@ jobs:
with:
fetch-depth: 0
# https://github.com/marketplace/actions/nerdbank-gitversioning
- name: Nerdbank.GitVersioning
uses: dotnet/nbgv@v0.4.2
with:
setAllVars: true
- uses: oven-sh/setup-bun@v2
# Configure Caching
- name: Setup cache environment
@ -89,23 +85,16 @@ jobs:
composer install --dev --no-interaction --no-scripts --verbose
composer global require laravel/pint
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
- name: Install NPM dependencies
run: |
npm install
npm run fmt config/version.yml
# Need to lint version.yml since its updated in CI
bun install
bun run fmt config/version.yml
- name: Lint & Format Check
run: npm run lint && npm run fmt:check
run: bun run lint && bun run fmt:check
- name: Compile assets
run: npm run build
run: bun run build
- name: Run Composer post-autoload scripts
run: |
@ -125,10 +114,6 @@ jobs:
- name: Run PHPStan
run: vendor/bin/phpstan analyse --memory-limit=2G
# - name: Run Rector
# run: |
# vendor/bin/rector --dry-run
# This runs after all of the tests, run have run. Creates a cleaned up version of the
# distro, and then creates the artifact to push up to S3 or wherever
artifacts:
@ -147,11 +132,7 @@ jobs:
with:
fetch-depth: 0
# https://github.com/marketplace/actions/nerdbank-gitversioning
- name: Nerdbank.GitVersioning
uses: dotnet/nbgv@v0.4.2
with:
setAllVars: true
- uses: oven-sh/setup-bun@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
@ -183,17 +164,11 @@ jobs:
composer install --no-dev --prefer-dist --no-interaction --no-scripts --verbose
sudo chmod +x ./.github/scripts/*
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
- name: Install NPM dependencies
run: npm install
run: bun install
- name: Compile assets
run: npm run build
run: bun run build
- name: Run Composer post-autoload scripts
run: composer dump-autoload
@ -245,53 +220,6 @@ jobs:
with:
fetch-depth: 0
# Configure Caching
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
# Three-step bootstrap (see `build` job comment for full rationale):
# 1. composer install --no-scripts: vendor/ for Vite CSS @imports
# 2. npm build: writes public/build manifest + dist files
# 3. composer dump-autoload: re-fires post-autoload-dump cleanly
- name: "Install Release Dependencies (no scripts)"
run: |
rm -rf vendor
composer install --no-dev --prefer-dist --no-interaction --no-scripts --verbose
sudo chmod +x ./.github/scripts/*
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
- name: Install NPM dependencies
run: npm install
- name: Compile assets
run: npm run build
- name: Run Composer post-autoload scripts
run: composer dump-autoload
# https://github.com/marketplace/actions/nerdbank-gitversioning
- name: Nerdbank.GitVersioning
uses: dotnet/nbgv@v0.4.2
with:
setAllVars: true
- id: version
name: Get version
run: .github/scripts/version.sh
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
@ -299,28 +227,16 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
- name: Build and push Docker images
uses: iloveitaly/github-action-railpack@master
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
push: false
tags: |
type=semver,pattern={{version}},value=${{ env.NBGV_SemVer2 }}
type=semver,pattern={{major}}.{{minor}},value=${{ env.NBGV_SemVer2 }}
type=ref,event=branch
${{ env.IMAGE_NAME }}:latest
- name: Build and push
id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
# - name: Generate artifact attestation
# uses: actions/attest-build-provenance@v2
# with:
# subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
# subject-digest: ${{ steps.push.outputs.digest }}
# push-to-registry: true

1
.gitignore vendored
View File

@ -107,3 +107,4 @@ modules_statuses.json
# Local SQLite scratch db (test artifact)
/phpvms
.env.testing
/intellij_style.xml

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Condition="!Exists('packages.config')">
<PrivateAssets>all</PrivateAssets>
<Version>3.7.115</Version>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -1,63 +0,0 @@
# This Dockerfile is used to create the base phpVMS image for Docker in production.
# It is based on https://serversideup.net/open-source/docker-php/.
FROM composer:latest AS vendor
LABEL org.opencontainers.image.description="The official phpVMS image"
COPY composer.json composer.json
COPY composer.lock composer.lock
COPY database database
RUN composer install \
--ignore-platform-reqs \
--no-interaction \
--no-plugins \
--no-scripts \
--prefer-dist
FROM serversideup/php:8.4-fpm
ARG WWWUSER=1000
ARG WWWGROUP=1000
# Switch to root so we can do root things
USER root
# Install missing extensions with root permissions
RUN install-php-extensions intl bcmath
# Install mariadb client (required for backups)
RUN apt-get update; \
apt-get upgrade -yqq; \
apt-get install -yqq --no-install-recommends --show-progress \
mariadb-client
# Install nodejs
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# Deal with permissions
RUN usermod -ou $WWWUSER www-data \
&& groupmod -og $WWWGROUP www-data
# Drop back to our unprivileged user
USER www-data
# Copy application files
COPY --chown=www-data:www-data . /var/www/html
# Copy deps from the composer build stage
COPY --chown=www-data:www-data --from=vendor /app/vendor/ /var/www/html/vendor/
# Build assets
# Note: the assets are moved to the build directory in the image, they'll be moved back to
# the public directory in the entrypoint script. This allows to overwrite the volume
# so that the assets are also updated in caddy.
RUN npm install && npm run build && \
cp -R ./public/build/ ./build
COPY --chmod=755 ./resources/docker/run-dump-autoload.sh /etc/entrypoint.d/20-run-dump-autoload.sh
COPY --chmod=755 ./resources/docker/copy-assets.sh /etc/entrypoint.d/30-copy-assets.sh

68
Dockerfile.prod Normal file
View File

@ -0,0 +1,68 @@
# This Dockerfile is used to create the base phpVMS image for Docker in production.
# It is based on https://serversideup.net/open-source/docker-php/.
#
# I highly recommend using the Railpack instead!
FROM serversideup/php:8.5-cli AS build
LABEL org.opencontainers.image.description="The official phpvms image"
USER root
COPY --from=oven/bun:latest /usr/local/bin/bun /usr/local/bin/bun
RUN install-php-extensions intl bcmath
WORKDIR /build
COPY . ./
RUN composer install \
--no-interaction \
--prefer-dist \
--no-dev \
--optimize-autoloader
RUN bun install && bun run build
# -----------------------------------------------------------------------------
#
# Final image
#
FROM serversideup/php:8.5-frankenphp
ARG WWWUSER=1000
ARG WWWGROUP=1000
# Switch to root so we can do root things
USER root
# Install missing extensions with root permissions
RUN install-php-extensions intl bcmath
# Install mariadb client (required for backups)
RUN apt-get update; \
apt-get upgrade -yqq; \
apt-get install -yqq --no-install-recommends --show-progress \
mariadb-client
COPY --from=oven/bun:latest /usr/local/bin/bun /usr/local/bin/bun
# Deal with permissions
RUN usermod -ou $WWWUSER www-data \
&& groupmod -og $WWWGROUP www-data
# Drop back to our unprivileged user
USER www-data
# Copy application files
COPY --chown=www-data:www-data --from=build /build/ /var/www/html/
COPY --chmod=755 ./resources/docker/run-dump-autoload.sh /etc/entrypoint.d/20-run-dump-autoload.sh
# The image keeps Serversideup's default FrankenPHP entrypoint (classic mode:
# one PHP worker per request, identical request semantics to the prior FPM
# setup). Production deployments override the container `command:` to launch
# Laravel Octane worker mode — see `compose.deploy.yml` for the canonical
# invocation. Octane is not started by this Dockerfile so that anyone
# pulling the bare image (k8s, custom Compose, Swarm) gets a predictable,
# documented runtime without surprise behaviour.

View File

@ -1,98 +0,0 @@
#
#
# Create the phpvms database if needed:
# docker exec phpvms /usr/bin/mysql -uroot -e 'CREATE DATABASE phpvms'
SHELL := /bin/bash
COMPOSER ?= $(shell which composer)
PKG_NAME := "/tmp"
CURR_PATH=$(shell pwd)
.PHONY: all
all: install
.PHONY: clean
clean:
@php artisan optimize:clear
@find bootstrap/cache -type f -not -name '.gitignore' -print0 | xargs -0 rm -rf
@find storage/framework/cache/ -mindepth 1 -type f -not -name '.gitignore' -print0 | xargs -0 rm -rf
@find storage/framework/sessions/ -mindepth 1 -type f -not -name '.gitignore' -print0 | xargs -0 rm -rf
@find storage/framework/views/ -mindepth 1 -not -name '.gitignore' -print0 | xargs -0 rm -rf
@find storage/logs -mindepth 1 -not -name '.gitignore' -print0 | xargs -0 rm -rf
.PHONY: clean-routes
clean-routes:
@php artisan route:clear
.PHONY: clear
clear:
@php artisan optimize:clear
.PHONY: build
build:
@php $(COMPOSER) install --no-interaction
# This is to build all the stylesheets, etc
.PHONY: build-assets
build-assets:
npm run build
.PHONY: install
install: build
@php artisan db:create
@php artisan migrate --seed
@echo "Done!"
.PHONY: update
update: build
@php $(COMPOSER) dump-autoload
@php $(COMPOSER) update --no-interaction
@php artisan migrate --force
@echo "Done!"
.PHONY: reset
reset: clean
@php $(COMPOSER) dump-autoload
@make reload-db
.PHONY: reload-db
reload-db:
@php artisan db:create --reset
@php artisan migrate --seed
@echo "Done!"
@make clean
.PHONY: tests
tests: test
.PHONY: test
test:
@#php artisan db:create --reset
@php artisan test -p
.PHONY: pint
pint:
@vendor/bin/pint --parallel
.PHONY: reset-installer
reset-installer:
@php artisan db:create --reset
@php artisan migrate:refresh --seed
.PHONY: docker-test
docker-test:
@docker run --rm \
-u $(shell id -u):$(shell id -g) \
-v $(shell pwd):/var/www/html \
-w /var/www/html \
laravelsail/php84-composer:latest \
composer install --ignore-platform-reqs
@vendor/bin/sail up
.PHONY: docker-clean
docker-clean:
-docker stop phpvms
-docker rm -rf phpvms
-rm -rf tmp/mysql

View File

@ -1,5 +0,0 @@
#dnsmasq: /usr/local/sbin/dnsmasq --keep-in-foreground
php-fpm: /usr/local/sbin/php-fpm --nodaemonize
nginx: /usr/local/bin/nginx -g 'daemon off;'
mysql: docker-compose --file ~/docker/mysql/docker-compose.yml up
#mailhog: /usr/local/bin/mailhog

View File

@ -36,6 +36,48 @@ A full distribution, with all the composer dependencies, is available at this
[View installation details](https://docs.phpvms.net/installation)
## Production Deployment with Docker
The reference production stack is `compose.deploy.yml`. It runs the official
phpVMS image (built from the repo `Dockerfile`, based on
`serversideup/php:8.5-frankenphp`) with Laravel Octane worker mode enabled,
plus MariaDB and Redis. No separate web-server sidecar is needed — FrankenPHP
serves HTTP/HTTPS directly.
```bash
docker compose -f compose.deploy.yml up -d
```
### Octane worker mode
The image itself ships with serversideup's default classic FrankenPHP entry
point (one PHP worker per request). `compose.deploy.yml` opts into Laravel
Octane worker mode via a `command:` override on the `app` service — the
[pattern documented upstream](https://serversideup.net/open-source/docker-php/docs/framework-guides/laravel/octane).
To fall back to classic FrankenPHP + PHP-worker mode (matches the prior
PHP-FPM request semantics — slower per request but bulletproof if a
worker-mode bug hits), delete the `command:` line from the `app` service
and recreate the container:
```yaml
services:
app:
# command: [...octane:start...] # remove this line
```
Under worker mode, `KvpService` (the JSON-backed key/value store used for
non-hot settings) is eventually consistent: writes from one worker
propagate to other workers on their next read of the same key. If your
deployment requires strong consistency for KVP, run with `--workers=1` or
remove the `command:` override.
Module authors: under Octane the framework stays booted across requests,
so avoid per-request data in singleton-bound services, `static` array
accumulators, and `boot()`-time toggles without a matching per-request
reset. See `app/Http/Middleware/DisableActivityLoggingByDefault.php` for
the pattern used to keep activity logging request-scoped.
## Development Environment with Docker
A full development environment can be brought up using Docker and

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Resets activity logging to "disabled" at the start of every request.
*
* Octane keeps the framework booted across requests, so a boot-time
* activity()->disableLogging() runs only once per worker. Routes / panels
* that flip logging on (EnableActivityLogging middleware, Filament's
* admin panel bootUsing) would then leave logging on for every subsequent
* request the worker handles. This middleware re-applies the default at
* the start of each request so the EnableActivityLogging middleware and
* Filament's per-panel toggle remain the explicit opt-ins.
*
* @phpstan-type Handler Closure(Request): Response
*/
class DisableActivityLoggingByDefault
{
/**
* @param Handler $next
*/
public function handle(Request $request, Closure $next): Response
{
activity()->disableLogging();
return $next($request);
}
}

View File

@ -41,14 +41,16 @@ class SetActiveTheme implements Middleware
}
/**
* Set the theme for the current middleware
* Set the theme for the current middleware.
*
* Octane keeps the application booted across requests, so the underlying
* igaster/laravel-theme singleton retains whatever theme the previous
* request set. Skipped paths (admin/api/install) must therefore reset
* to the configured default explicitly early-returning would let a
* prior frontend request's theme leak into the next admin/api response.
*/
public function setTheme(Request $request): void
{
if ($request->is(self::$skip)) {
return;
}
try {
$theme = setting('general.theme', 'seven');
} catch (Exception $exception) {
@ -56,8 +58,18 @@ class SetActiveTheme implements Middleware
$theme = 'seven';
}
if (!empty($theme)) {
if (empty($theme)) {
$theme = config('themes.default');
}
if ($request->is(self::$skip)) {
// Skipped paths don't pick a per-request theme but still need a
// deterministic baseline under Octane (see method PHPDoc).
Theme::set(config('themes.default'));
return;
}
Theme::set($theme);
}
}
}

View File

@ -72,7 +72,13 @@ class AppServiceProvider extends ServiceProvider
Paginator::useBootstrap();
activity()->disableLogging();
// Activity logging defaults to disabled for every request. The reset
// lives in App\Http\Middleware\DisableActivityLoggingByDefault (wired
// in bootstrap/app.php) instead of here, because Octane runs boot()
// only once per worker — the per-request reset has to live in the
// request pipeline. The middleware reapplies the default before any
// opt-in (EnableActivityLogging middleware, Filament panel boot)
// gets a chance to flip it on.
/**
* Inject the extra display + monospace fonts used by the docs design

View File

@ -10,6 +10,13 @@ class KvpService
{
private readonly Valuestore $valueStore;
// TODO(octane): re-resolve per request or move to Redis-backed KV.
// Spatie\Valuestore is a JSON-file wrapper that caches parsed contents
// in-memory after writes. Under Octane the same instance lives for the
// worker's lifetime, so writes from worker A propagate to worker B only
// on B's next read of the same key — KVP is eventually consistent under
// worker mode. See README.md > "Octane worker mode" for the operator-
// facing note.
public function __construct()
{
$this->valueStore = Valuestore::make(config('phpvms.kvp_storage_path'));

View File

@ -22,12 +22,16 @@ use PharData;
class ModuleService extends Service
{
protected static array $adminLinks = [];
/**
* Module-registered admin nav links. Populated once per worker via each
* module's boot()->registerLinks() call; not a per-request accumulator.
*/
protected array $adminLinks = [];
/**
* @var array 0 == logged out, 1 == logged in
*/
protected static array $frontendLinks = [
protected array $frontendLinks = [
0 => [],
1 => [],
];
@ -37,7 +41,7 @@ class ModuleService extends Service
*/
public function addFrontendLink(string $title, string $url, string $icon = 'bi bi-people', bool $logged_in = true): void
{
self::$frontendLinks[$logged_in][] = [
$this->frontendLinks[$logged_in][] = [
'title' => $title,
'url' => $url,
'icon' => $icon,
@ -49,7 +53,7 @@ class ModuleService extends Service
*/
public function getFrontendLinks(mixed $logged_in): array
{
return self::$frontendLinks[$logged_in];
return $this->frontendLinks[$logged_in];
}
/**
@ -57,7 +61,7 @@ class ModuleService extends Service
*/
public function addAdminLink(string $title, string $url, string $icon = 'bi bi-people'): void
{
self::$adminLinks[] = [
$this->adminLinks[] = [
'title' => $title,
'url' => $url,
'icon' => $icon,
@ -69,7 +73,7 @@ class ModuleService extends Service
*/
public function getAdminLinks(): array
{
return self::$adminLinks;
return $this->adminLinks;
}
/**

View File

@ -7,6 +7,7 @@ use App\Exceptions\Converters\SymfonyException;
use App\Exceptions\Converters\ValidationException;
use App\Exceptions\Unauthenticated;
use App\Http\Middleware\ApiAuth;
use App\Http\Middleware\DisableActivityLoggingByDefault;
use App\Http\Middleware\InstalledCheck;
use App\Http\Middleware\SetActiveLanguage;
use App\Http\Middleware\SetActiveTheme;
@ -33,6 +34,14 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
// Prepend the activity-logging reset to both groups so every request
// starts with logging disabled. EnableActivityLogging (and Filament's
// panel boot hook) then opt in for the request that wants it. Required
// for Octane safety — see config/octane.php and the change at
// openspec/changes/switch-to-frankenphp-image/design.md.
$middleware->prependToGroup('web', DisableActivityLoggingByDefault::class);
$middleware->prependToGroup('api', DisableActivityLoggingByDefault::class);
$middleware->appendToGroup('web', [
InstalledCheck::class,
SetActiveTheme::class,

218
bun.lock
View File

@ -4,7 +4,9 @@
"workspaces": {
"": {
"dependencies": {
"axios": "^1.15.0",
"@date-fns/tz": "^1.5.0",
"@preact/signals": "^2.9.0",
"axios": "^1.15",
"bootstrap": "~4.6",
"chart.js": "^4.5.1",
"chartjs-adapter-date-fns": "^3.0.0",
@ -18,22 +20,72 @@
"leaflet.geodesic": "^2.5.2",
"moment": "^2.29.1",
"popper.js": "^1.15.0",
"preact": "^10.29.2",
"rivets": "^0.9.6",
"select2": "^4.0.13",
},
"devDependencies": {
"@preact/preset-vite": "^2.10.5",
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^25.9.1",
"esbuild": "^0.28.0",
"happy-dom": "^20.9.0",
"laravel-vite-plugin": "^3.0.0",
"oxfmt": "^0.41.0",
"oxlint": "^1.56.0",
"sass": "^1.54.5",
"tailwindcss": "^4.1.11",
"typescript": "^6.0.3",
"vite": "^8.0.10",
"vitest": "^4.1.7",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
"@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
"@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
"@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
"@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/types": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A=="],
"@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.29.7", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g=="],
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
"@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
"@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
"@date-fns/tz": ["@date-fns/tz@1.5.0", "", {}, "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg=="],
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
@ -212,6 +264,20 @@
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
"@preact/preset-vite": ["@preact/preset-vite@2.10.5", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@prefresh/vite": "^2.4.11", "@rollup/pluginutils": "^5.0.0", "babel-plugin-transform-hook-names": "^1.0.2", "debug": "^4.4.3", "magic-string": "^0.30.21", "picocolors": "^1.1.1", "vite-prerender-plugin": "^0.5.8", "zimmerframe": "^1.1.4" }, "peerDependencies": { "@babel/core": "7.x", "vite": "2.x || 3.x || 4.x || 5.x || 6.x || 7.x || 8.x" } }, "sha512-p0vJpxiVO7KWWazWny3LUZ+saXyZKWv6Ju0bYMWNJRp2YveufRPgSUB1C4MTqGJfz07EehMgfN+AJNwQy+w6Iw=="],
"@preact/signals": ["@preact/signals@2.9.1", "", { "dependencies": { "@preact/signals-core": "^1.14.0" }, "peerDependencies": { "preact": ">= 10.25.0 || >=11.0.0-0" } }, "sha512-xVqN8mJjbSN5IB/8Ubmd9NN+Ew6zJswoRxrjZbH3YsgkMshFeO6d8zxEFpHRTq9GJZx7cnPs2CnCpFqtGXGNsw=="],
"@preact/signals-core": ["@preact/signals-core@1.14.2", "", {}, "sha512-RZHdBj9ZF4n40Rp4jS052EHHjBWf96P9oNdXPfhQTovCuWY9iQn3Gq+gOTJSgBO9A/JBuPfMOWsSX/lIU9Pc/A=="],
"@prefresh/babel-plugin": ["@prefresh/babel-plugin@0.5.3", "", {}, "sha512-57LX2SHs4BX2s1IwCjNzTE2OJeEepRCNf1VTEpbNcUyHfMO68eeOWGDIt4ob9aYlW6PEWZ1SuwNikuoIXANDtQ=="],
"@prefresh/core": ["@prefresh/core@1.5.10", "", { "peerDependencies": { "preact": "^10.0.0 || ^11.0.0-0" } }, "sha512-7yPTFbG56sutaFu8krp3B4a200KOFUvrtlllKWRuLjsYXo9UUucHOZRcer+gtgMkFTpv6ob8TGcTwA32bSwa1w=="],
"@prefresh/utils": ["@prefresh/utils@1.2.1", "", {}, "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw=="],
"@prefresh/vite": ["@prefresh/vite@2.4.12", "", { "dependencies": { "@babel/core": "^7.22.1", "@prefresh/babel-plugin": "^0.5.2", "@prefresh/core": "^1.5.0", "@prefresh/utils": "^1.2.0", "@rollup/pluginutils": "^4.2.1" }, "peerDependencies": { "preact": "^10.4.0 || ^11.0.0-0", "vite": ">=2.0.0" } }, "sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="],
@ -244,6 +310,10 @@
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
@ -276,14 +346,54 @@
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@vitest/expect": ["@vitest/expect@4.1.7", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.7", "@vitest/utils": "4.1.7", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w=="],
"@vitest/mocker": ["@vitest/mocker@4.1.7", "", { "dependencies": { "@vitest/spy": "4.1.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.7", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw=="],
"@vitest/runner": ["@vitest/runner@4.1.7", "", { "dependencies": { "@vitest/utils": "4.1.7", "pathe": "^2.0.3" } }, "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw=="],
"@vitest/snapshot": ["@vitest/snapshot@4.1.7", "", { "dependencies": { "@vitest/pretty-format": "4.1.7", "@vitest/utils": "4.1.7", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw=="],
"@vitest/spy": ["@vitest/spy@4.1.7", "", {}, "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q=="],
"@vitest/utils": ["@vitest/utils@4.1.7", "", { "dependencies": { "@vitest/pretty-format": "4.1.7", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"axios": ["axios@1.15.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="],
"babel-plugin-transform-hook-names": ["babel-plugin-transform-hook-names@1.0.2", "", { "peerDependencies": { "@babel/core": "^7.12.10" } }, "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"bootstrap": ["bootstrap@4.6.2", "", { "peerDependencies": { "jquery": "1.9.1 - 3", "popper.js": "^1.16.1" } }, "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ=="],
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
"chartjs-adapter-date-fns": ["chartjs-adapter-date-fns@3.0.0", "", { "peerDependencies": { "chart.js": ">=2.8.0", "date-fns": ">=2.0.0" } }, "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg=="],
@ -294,28 +404,56 @@
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookieconsent": ["cookieconsent@3.1.1", "", {}, "sha512-v8JWLJcI7Zs9NWrs8hiVldVtm3EBF70TJI231vxn6YToBGj0c9dvdnYwltydkAnrbBMOM/qX1xLFrnTfm5wTag=="],
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"electron-to-chromium": ["electron-to-chromium@1.5.363", "", {}, "sha512-VjUKPyWzGnT1fujlkEGC/BvN70Hh70KXtAqcmniXviYlJC/ivcT+BWGPyxWVbJZLfvtKR6dqg1L7T7pgAMBtWA=="],
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": "bin/esbuild" }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
@ -326,6 +464,8 @@
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
@ -334,12 +474,16 @@
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"happy-dom": ["happy-dom@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
"immutable": ["immutable@5.1.5", "", {}, "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
@ -350,6 +494,14 @@
"jquery": ["jquery@3.7.1", "", {}, "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
"laravel-vite-plugin": ["laravel-vite-plugin@3.0.0", "", { "dependencies": { "picocolors": "^1.0.0", "tinyglobby": "^0.2.12", "vite-plugin-full-reload": "^1.1.0" }, "peerDependencies": { "vite": "^8.0.0" }, "bin": { "clean-orphaned-assets": "bin/clean.js" } }, "sha512-eO6B/IPH448XNjyA5OmJsFAeJH3MeQGWodNMzejx44+rhV44cs0op05t4yku9ifaRnikDkSHLOLXszJL0tiW3w=="],
"leaflet": ["leaflet@1.9.4", "", {}, "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="],
@ -384,6 +536,8 @@
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@ -394,14 +548,26 @@
"moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
"node-html-parser": ["node-html-parser@6.1.13", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg=="],
"node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"oxfmt": ["oxfmt@0.41.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.41.0", "@oxfmt/binding-android-arm64": "0.41.0", "@oxfmt/binding-darwin-arm64": "0.41.0", "@oxfmt/binding-darwin-x64": "0.41.0", "@oxfmt/binding-freebsd-x64": "0.41.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.41.0", "@oxfmt/binding-linux-arm-musleabihf": "0.41.0", "@oxfmt/binding-linux-arm64-gnu": "0.41.0", "@oxfmt/binding-linux-arm64-musl": "0.41.0", "@oxfmt/binding-linux-ppc64-gnu": "0.41.0", "@oxfmt/binding-linux-riscv64-gnu": "0.41.0", "@oxfmt/binding-linux-riscv64-musl": "0.41.0", "@oxfmt/binding-linux-s390x-gnu": "0.41.0", "@oxfmt/binding-linux-x64-gnu": "0.41.0", "@oxfmt/binding-linux-x64-musl": "0.41.0", "@oxfmt/binding-openharmony-arm64": "0.41.0", "@oxfmt/binding-win32-arm64-msvc": "0.41.0", "@oxfmt/binding-win32-ia32-msvc": "0.41.0", "@oxfmt/binding-win32-x64-msvc": "0.41.0" }, "bin": "bin/oxfmt" }, "sha512-sKLdJZdQ3bw6x9qKiT7+eID4MNEXlDHf5ZacfIircrq6Qwjk0L6t2/JQlZZrVHTXJawK3KaMuBoJnEJPcqCEdg=="],
"oxlint": ["oxlint@1.56.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.56.0", "@oxlint/binding-android-arm64": "1.56.0", "@oxlint/binding-darwin-arm64": "1.56.0", "@oxlint/binding-darwin-x64": "1.56.0", "@oxlint/binding-freebsd-x64": "1.56.0", "@oxlint/binding-linux-arm-gnueabihf": "1.56.0", "@oxlint/binding-linux-arm-musleabihf": "1.56.0", "@oxlint/binding-linux-arm64-gnu": "1.56.0", "@oxlint/binding-linux-arm64-musl": "1.56.0", "@oxlint/binding-linux-ppc64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-musl": "1.56.0", "@oxlint/binding-linux-s390x-gnu": "1.56.0", "@oxlint/binding-linux-x64-gnu": "1.56.0", "@oxlint/binding-linux-x64-musl": "1.56.0", "@oxlint/binding-openharmony-arm64": "1.56.0", "@oxlint/binding-win32-arm64-msvc": "1.56.0", "@oxlint/binding-win32-ia32-msvc": "1.56.0", "@oxlint/binding-win32-x64-msvc": "1.56.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": "bin/oxlint" }, "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
@ -410,6 +576,8 @@
"postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
"preact": ["preact@10.29.2", "", {}, "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ=="],
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
@ -422,24 +590,72 @@
"select2": ["select2@4.0.13", "", {}, "sha512-1JeB87s6oN/TDxQQYCvS5EFoQyvV6eYMZZ0AeA4tdFDYWN3BAGZ8npr17UBFddU0lgAt3H0yjX3X6/ekOj1yjw=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"sightglass": ["sightglass@0.2.6", "", {}, "sha512-t1fgbuhURcWc8VgZk8kJQ3QmmZk3kghDcf0wpsN8I8RaV05IUkc2b195KpGqgocKT/q8+vKk6EcB2c7N2lAd6A=="],
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"simple-code-frame": ["simple-code-frame@1.3.0", "", { "dependencies": { "kolorist": "^1.6.0" } }, "sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"stack-trace": ["stack-trace@1.0.0", "", {}, "sha512-H6D7134xi6qONvh7ZHKgviXf+rd3vhGBSvebPZCaUkd8zvQ+7PtDw6CljPTe4cXWNf2IKZGNqw6VJXSb9IgBpA=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="],
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@1.2.2", "", {}, "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "less", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="],
"vite-plugin-full-reload": ["vite-plugin-full-reload@1.2.0", "", { "dependencies": { "picocolors": "^1.0.0", "picomatch": "^2.3.1" } }, "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA=="],
"vite-prerender-plugin": ["vite-prerender-plugin@0.5.13", "", { "dependencies": { "kolorist": "^1.8.0", "magic-string": "0.x >= 0.26.0", "node-html-parser": "^6.1.12", "simple-code-frame": "^1.3.0", "source-map": "^0.7.4", "stack-trace": "^1.0.0-pre2" }, "peerDependencies": { "vite": "5.x || 6.x || 7.x || 8.x" } }, "sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g=="],
"vitest": ["vitest@4.1.7", "", { "dependencies": { "@vitest/expect": "4.1.7", "@vitest/mocker": "4.1.7", "@vitest/pretty-format": "4.1.7", "@vitest/runner": "4.1.7", "@vitest/snapshot": "4.1.7", "@vitest/spy": "4.1.7", "@vitest/utils": "4.1.7", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.7", "@vitest/browser-preview": "4.1.7", "@vitest/browser-webdriverio": "4.1.7", "@vitest/coverage-istanbul": "4.1.7", "@vitest/coverage-v8": "4.1.7", "@vitest/ui": "4.1.7", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA=="],
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
"@prefresh/vite/@rollup/pluginutils": ["@rollup/pluginutils@4.2.1", "", { "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" } }, "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ=="],
"@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"vite-plugin-full-reload/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"@prefresh/vite/@rollup/pluginutils/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
}
}

192
compose.deploy.yml Normal file
View File

@ -0,0 +1,192 @@
# Production Reference Compose stack for phpVMS.
#
# The `app` service runs the FrankenPHP image with Laravel Octane worker
# mode enabled via a `command:` override (the upstream-documented way to
# launch Octane with serversideup/php:8.5-frankenphp). Octane keeps the
# framework booted across requests, which materially helps Filament /
# Livewire-heavy admin paths.
#
# To fall back to classic FrankenPHP + PHP-worker mode (matches the prior
# PHP-FPM request semantics — slower but bulletproof if an Octane bug hits):
# delete the `command:` line below and recreate the `app` container. No
# rebuild is required.
#
# Depending on your host, you might have to enter the contents of `.env`
# into their environment variable configuration; for example, that's the
# case for Railway or Dokploy. Dokploy creates a `.env` from the environment
# variables that you set in the interface, so the reading of `.env` is required
# regardless
#
# This compose file also includes a MariaDB and Redis container. You can
# run this within this stack, if you really want to. You *must* back up the
# Docker volumes listed at the bottom, but I highly recommend against it.
# Production deployments should use dedicated services for these components.
#
# For local development, see `compose.sail.yml` (Laravel Sail).
#
services:
app:
# If you want to use the remote image from Docker Hub, comment out this line
# or set it to "always" to pull the latest version
pull_policy: build
# Build from the local repository
image: ${PHPVMS_IMAGE_NAME:-phpvms/phpvms:latest}
# Build from the local repository
build:
context: .
dockerfile: Dockerfile.prod
args:
WWWUSER: "${WWWUSER:-1000}"
WWWGROUP: "${WWWGROUP:-1000}"
restart: unless-stopped
environment:
AUTORUN_ENABLED: "true"
# Enable this if you're having startup trouble...
AUTORUN_DEBUG: "false"
# OPcache tuned for Octane: code is loaded once and stays in memory, so
# disable timestamp revalidation (deploys replace the container) and
# raise the file cap to cover Filament + module trees.
PHP_OPCACHE_ENABLE: "1"
PHP_OPCACHE_VALIDATE_TIMESTAMPS: "0"
PHP_OPCACHE_MAX_ACCELERATED_FILES: "20000"
PHP_OPCACHE_INTERNED_STRINGS_BUFFER: "16"
PHP_OPCACHE_JIT: "tracing"
PHP_OPCACHE_JIT_BUFFER_SIZE: "64M"
# For Caddy, no need to change
XDG_DATA_HOME: "/var/www/html/storage"
XDG_CONFIG_HOME: "/var/www/html/storage"
# This is the database connection to use
DB_CONNECTION: mysql
DB_HOST: ${DB_HOST:-mysql}
DB_PORT: 3306
DB_DATABASE: ${DB_DATABASE:-phpvms}
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
# Delete this line to revert to classic FrankenPHP + PHP-worker mode.
command:
[
"php",
"/var/www/html/artisan",
"octane:start",
"--server=frankenphp",
"--host=0.0.0.0",
"--workers=auto",
"--max-requests=500",
]
ports:
# The image binds to 8000/8443 internally (unprivileged); host ports
# 80/443 are mapped at the Compose layer.
- "${FORWARD_HTTP_PORT:-80}:8000"
- "${FORWARD_HTTPS_PORT:-443}:8443"
user: "${WWWUSER:-1000}:${WWWGROUP:-1000}"
env_file:
- .env
volumes:
- ./modules:/var/www/html/modules:ro
- phpvms_uploads:/var/www/html/public/uploads:rw
- phpvms_storage:/var/www/html/storage:rw
depends_on:
- mariadb
- redis
healthcheck:
# Native health check script for Octane shipped by serversideup/php.
test: ["CMD", "healthcheck-octane"]
start_period: 30s
interval: 30s
timeout: 5s
retries: 3
task:
image: ${PHPVMS_IMAGE_NAME:-phpvms/phpvms:latest}
user: "${WWWUSER:-1000}:${WWWGROUP:-1000}"
restart: unless-stopped
command: ["php", "/var/www/html/artisan", "schedule:work"]
environment:
PHP_OPCACHE_ENABLE: "1"
healthcheck:
# This is our native health check script for the scheduler
test: ["CMD", "healthcheck-schedule"]
start_period: 10s
env_file: .env
volumes:
- ./modules:/var/www/html/modules:ro
- phpvms_uploads:/var/www/html/public/uploads:rw
- phpvms_storage:/var/www/html/storage:rw
queue:
image: ${PHPVMS_IMAGE_NAME:-phpvms/phpvms:latest}
user: "${WWWUSER:-1000}:${WWWGROUP:-1000}"
restart: unless-stopped
command: ["php", "/var/www/html/artisan", "queue:work", "--tries=3"]
environment:
PHP_OPCACHE_ENABLE: "1"
healthcheck:
# This is our native health check script for the queue
test: ["CMD", "healthcheck-queue"]
start_period: 10s
env_file: .env
volumes:
- ./modules:/var/www/html/modules:ro
- phpvms_uploads:/var/www/html/public/uploads:rw
- phpvms_storage:/var/www/html/storage:rw
# ----------------------------------------------------------
# BELOW HERE ARE THE CONTAINERS FOR THE DATABASE AND REDIS
#
# If you are planning to run this in production, I **highly**
# recommend using a managed database service like AWS RDS or
# Google Cloud SQL. These services offer high availability,
# automatic backups, and other features that are not available
# with self-hosted solutions.
#
# ----------------------------------------------------------
# You can also use PostgreSQL
mariadb:
hostname: mysql
image: mariadb:11.8 # lts
restart: unless-stopped
expose:
- 3306
environment:
MYSQL_ROOT_PASSWORD: "${DB_PASSWORD}"
MYSQL_DATABASE: "${DB_DATABASE}"
MYSQL_USER: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
MYSQL_ALLOW_EMPTY_PASSWORD: "no"
volumes:
- database:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 10s
interval: 10s
timeout: 5s
retries: 3
redis:
container_name: phpvms-redis
image: redis:8.8-alpine3.23
restart: unless-stopped
volumes:
- redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
retries: 3
timeout: 5s
volumes:
database:
phpvms_storage:
phpvms_uploads:
redis:

View File

@ -95,7 +95,8 @@
"opcodesio/log-viewer": "^3.24",
"spatie/laravel-data": "^4.22.1",
"jlorente/laravel-data-migrations": "^2.0",
"hidehalo/nanoid-php": "^2.0"
"hidehalo/nanoid-php": "^2.0",
"laravel/octane": "^2.13"
},
"require-dev": {
"filp/whoops": "^2.18.4",
@ -152,9 +153,9 @@
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi",
"@php artisan ide-helper:generate",
"@php artisan ide-helper:meta",
".hooks/setup-git-hooks.sh"
"@php artisan ide-helper:generate || true",
"@php artisan ide-helper:meta || true",
".hooks/setup-git-hooks.sh || true"
],
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
@ -172,7 +173,7 @@
],
"bump:minor": [
"composer update && composer bump",
"npx npm-check-updates -u -t minor && npm install"
"bum x npm-check-updates -u -t minor && npm install"
],
"lint": "pint --parallel",
"pint": "pint --parallel",

776
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -57,12 +57,13 @@ return [
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix' => env('DB_PREFIX', ''),
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
Mysql::ATTR_PERSISTENT => true, // Critical for Octane
]) : [],
],
@ -83,6 +84,7 @@ return [
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
Mysql::ATTR_PERSISTENT => true, // Critical for Octane
]) : [],
],

239
config/octane.php Normal file
View File

@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
use Laravel\Octane\Contracts\OperationTerminated;
use Laravel\Octane\Events\RequestHandled;
use Laravel\Octane\Events\RequestReceived;
use Laravel\Octane\Events\RequestTerminated;
use Laravel\Octane\Events\TaskReceived;
use Laravel\Octane\Events\TaskTerminated;
use Laravel\Octane\Events\TickReceived;
use Laravel\Octane\Events\TickTerminated;
use Laravel\Octane\Events\WorkerErrorOccurred;
use Laravel\Octane\Events\WorkerStarting;
use Laravel\Octane\Events\WorkerStopping;
use Laravel\Octane\Listeners\CloseMonologHandlers;
use Laravel\Octane\Listeners\CollectGarbage;
use Laravel\Octane\Listeners\DisconnectFromDatabases;
use Laravel\Octane\Listeners\EnsureUploadedFilesAreValid;
use Laravel\Octane\Listeners\EnsureUploadedFilesCanBeMoved;
use Laravel\Octane\Listeners\FlushOnce;
use Laravel\Octane\Listeners\FlushTemporaryContainerInstances;
use Laravel\Octane\Listeners\FlushUploadedFiles;
use Laravel\Octane\Listeners\ReportException;
use Laravel\Octane\Listeners\StopWorkerIfNecessary;
use Laravel\Octane\Octane;
return [
/*
|--------------------------------------------------------------------------
| Octane Server
|--------------------------------------------------------------------------
|
| This value determines the default "server" that will be used by Octane
| when starting, restarting, or stopping your server via the CLI. You
| are free to change this to the supported server of your choosing.
|
| Supported: "roadrunner", "swoole", "frankenphp"
|
*/
'server' => env('OCTANE_SERVER', 'frankenphp'),
/*
|--------------------------------------------------------------------------
| Force HTTPS
|--------------------------------------------------------------------------
|
| When this configuration value is set to "true", Octane will inform the
| framework that all absolute links must be generated using the HTTPS
| protocol. Otherwise your links may be generated using plain HTTP.
|
*/
'https' => env('OCTANE_HTTPS', false),
/*
|--------------------------------------------------------------------------
| Octane Listeners
|--------------------------------------------------------------------------
|
| All of the event listeners for Octane's events are defined below. These
| listeners are responsible for resetting your application's state for
| the next request. You may even add your own listeners to the list.
|
| NOTE: Octane::prepareApplicationForNextRequest() includes FlushLocaleState,
| which is what stops App::setLocale() calls in middleware (e.g.
| App\Http\Middleware\SetActiveLanguage) from leaking between requests
| served by the same worker. Do NOT drop the spread call below or replace
| it with a hand-curated list that omits FlushLocaleState see the
| "Octane Safety Audit" notes for context.
|
*/
'listeners' => [
WorkerStarting::class => [
EnsureUploadedFilesAreValid::class,
EnsureUploadedFilesCanBeMoved::class,
],
RequestReceived::class => [
...Octane::prepareApplicationForNextOperation(),
...Octane::prepareApplicationForNextRequest(),
//
],
RequestHandled::class => [
//
],
RequestTerminated::class => [
// FlushUploadedFiles::class,
],
TaskReceived::class => [
...Octane::prepareApplicationForNextOperation(),
//
],
TaskTerminated::class => [
//
],
TickReceived::class => [
...Octane::prepareApplicationForNextOperation(),
//
],
TickTerminated::class => [
//
],
OperationTerminated::class => [
FlushOnce::class,
FlushTemporaryContainerInstances::class,
// DisconnectFromDatabases::class,
// CollectGarbage::class,
],
WorkerErrorOccurred::class => [
ReportException::class,
StopWorkerIfNecessary::class,
],
WorkerStopping::class => [
CloseMonologHandlers::class,
],
],
/*
|--------------------------------------------------------------------------
| Warm / Flush Bindings
|--------------------------------------------------------------------------
|
| The bindings listed below will either be pre-warmed when a worker boots
| or they will be flushed before every new request. Flushing a binding
| will force the container to resolve that binding again when asked.
|
*/
'warm' => [
...Octane::defaultServicesToWarm(),
],
'flush' => [
//
],
/*
|--------------------------------------------------------------------------
| Octane Swoole Tables
|--------------------------------------------------------------------------
|
| While using Swoole, you may define additional tables as required by the
| application. These tables can be used to store data that needs to be
| quickly accessed by other workers on the particular Swoole server.
|
*/
'tables' => [
'example:1000' => [
'name' => 'string:1000',
'votes' => 'int',
],
],
/*
|--------------------------------------------------------------------------
| Octane Swoole Cache Table
|--------------------------------------------------------------------------
|
| While using Swoole, you may leverage the Octane cache, which is powered
| by a Swoole table. You may set the maximum number of rows as well as
| the number of bytes per row using the configuration options below.
|
*/
'cache' => [
'rows' => 1000,
'bytes' => 10000,
],
/*
|--------------------------------------------------------------------------
| File Watching
|--------------------------------------------------------------------------
|
| The following list of files and directories will be watched when using
| the --watch option offered by Octane. If any of the directories and
| files are changed, Octane will automatically reload your workers.
|
| The list below reflects the phpVMS layout: app/, bootstrap/, config/,
| database/, modules/, resources/views/, and routes/ are the directories
| a code change can land in. composer.lock and .env are watched so worker
| reload picks up dependency and env changes too.
|
*/
'watch' => [
'app',
'bootstrap',
'config/**/*.php',
'database/**/*.php',
'modules/**/*.php',
'resources/views/**/*.php',
'resources/views/**/*.blade.php',
'routes',
'composer.lock',
'.env',
],
/*
|--------------------------------------------------------------------------
| Garbage Collection Threshold
|--------------------------------------------------------------------------
|
| When executing long-lived PHP scripts such as Octane, memory can build
| up before being cleared by PHP. You can force Octane to run garbage
| collection if your application consumes this amount of megabytes.
|
*/
'garbage' => 50,
/*
|--------------------------------------------------------------------------
| Maximum Execution Time
|--------------------------------------------------------------------------
|
| The following setting configures the maximum execution time for requests
| being handled by Octane. You may set this value to 0 to indicate that
| there isn't a specific time limit on Octane request execution time.
|
*/
'max_execution_time' => 30,
];

View File

@ -8,5 +8,4 @@ return [
'default' => env('DEFAULT_THEME', 'seven'),
'cache' => true,
'themes' => [],
];

View File

@ -245,35 +245,35 @@ expenses:
amount: 100
type: F
active: 1
ref_model: App\Models\Expense
ref_model_type: App\Models\Expense
- name: Per-Flight (multiplier)
amount: 100
type: F
multiplier: 1
active: 1
ref_model: App\Models\Expense
ref_model_type: App\Models\Expense
- name: Per-Flight (multiplier, on airline)
airline_id: 1
amount: 200
type: F
multiplier: 1
active: 1
ref_model: App\Models\Expense
ref_model_type: App\Models\Expense
- name: A daily fee
amount: 800
type: D
active: 1
ref_model: App\Models\Expense
ref_model_type: App\Models\Expense
- name: A monthly fee
amount: 5000
type: M
active: 1
ref_model: App\Models\Expense
ref_model_type: App\Models\Expense
- name: Catering
amount: 1000
type: F
active: 1
ref_model: App\Models\Subfleet
ref_model_type: App\Models\Subfleet
ref_model_id: 1
created_at: now
updated_at: now
@ -281,7 +281,7 @@ expenses:
amount: 1000
type: D
active: 1
ref_model: App\Models\Subfleet
ref_model_type: App\Models\Subfleet
ref_model_id: 1
created_at: now
updated_at: now
@ -289,7 +289,7 @@ expenses:
amount: 1000
type: D
active: 1
ref_model: App\Models\Aircraft
ref_model_type: App\Models\Aircraft
ref_model_id: 1
created_at: now
updated_at: now
@ -861,7 +861,7 @@ journal_transactions:
currency: USD
memo: "Pilot Payment @ 50"
tags: "pilot_pay"
ref_model: App\Models\Pirep
ref_model_type: App\Models\Pirep
ref_model_id: pirepid_1
created_at: "2018-03-06 12:34:15"
updated_at: "2018-03-06 12:34:15"
@ -874,7 +874,7 @@ journal_transactions:
currency: USD
memo: "Expense: Per-Flight (no muliplier)"
tags: "expense"
ref_model: App\Models\Pirep
ref_model_type: App\Models\Pirep
ref_model_id: pirepid_1
created_at: "2018-03-06 12:34:15"
updated_at: "2018-03-06 12:34:15"
@ -887,7 +887,7 @@ journal_transactions:
currency: USD
memo: "Fares Y300; price: 200, cost: 0"
tags: "fare"
ref_model: App\Models\Pirep
ref_model_type: App\Models\Pirep
ref_model_id: pirepid_1
created_at: "2018-03-06 12:34:15"
updated_at: "2018-03-06 12:34:15"
@ -900,7 +900,7 @@ journal_transactions:
currency: USD
memo: "Expense: Per-Flight (multiplier, on airline)"
tags: "expense"
ref_model: App\Models\Pirep
ref_model_type: App\Models\Pirep
ref_model_id: pirepid_1
created_at: "2018-03-06 12:34:15"
updated_at: "2018-03-06 12:34:15"
@ -913,7 +913,7 @@ journal_transactions:
currency: USD
memo: "Expense: Per-Flight (multiplier)"
tags: "expense"
ref_model: App\Models\Pirep
ref_model_type: App\Models\Pirep
ref_model_id: pirepid_1
created_at: "2018-03-06 12:34:15"
updated_at: "2018-03-06 12:34:15"
@ -926,7 +926,7 @@ journal_transactions:
currency: USD
memo: "Subfleet Expense: Catering (747-43X RB211-524G)"
tags: "subfleet"
ref_model: App\Models\Pirep
ref_model_type: App\Models\Pirep
ref_model_id: pirepid_1
created_at: "2018-03-06 12:34:15"
updated_at: "2018-03-06 12:34:15"
@ -939,7 +939,7 @@ journal_transactions:
currency: USD
memo: "Fares F10; price: 1000, cost: 0"
tags: "fare"
ref_model: App\Models\Pirep
ref_model_type: App\Models\Pirep
ref_model_id: pirepid_1
created_at: "2018-03-06 12:34:15"
updated_at: "2018-03-06 12:34:15"
@ -952,7 +952,7 @@ journal_transactions:
currency: USD
memo: "Pilot Payment @ 50"
tags: "pilot_pay"
ref_model: App\Models\Pirep
ref_model_type: App\Models\Pirep
ref_model_id: pirepid_1
created_at: now
updated_at: now
@ -965,7 +965,7 @@ journal_transactions:
currency: USD
memo: "Fares B10; price: 1100, cost: 0"
tags: "fare"
ref_model: App\Models\Pirep
ref_model_type: App\Models\Pirep
ref_model_id: pirepid_1
created_at: now
updated_at: now
@ -978,7 +978,7 @@ journal_transactions:
currency: USD
memo: "Ground Handling"
tags: "ground_handling"
ref_model: App\Models\Pirep
ref_model_type: App\Models\Pirep
ref_model_id: pirepid_1
created_at: now
updated_at: now
@ -992,7 +992,7 @@ journal_transactions:
currency: USD
memo: "Subfleet 744-3X-RB211: Block Time Cost"
tags: "subfleet"
ref_model: App\Models\Pirep
ref_model_type: App\Models\Pirep
ref_model_id: pirepid_1
created_at: now
updated_at: now

View File

@ -1,133 +0,0 @@
# This Docker Compose file is an example of what a setup for running phpVMS in production could look like.
# It is based on the image created in the Dockerfile. You can find more examples in phpvms' documentation.
services:
app:
container_name: phpvms-app
user: "${WWWUSER:-1000}:${WWWGROUP:-1000}"
image: ghcr.io/phpvms/phpvms:main
hostname: app
restart: unless-stopped
environment:
PHP_OPCACHE_ENABLE: 1
# some basic laravel stuff
AUTORUN_ENABLED: 1
env_file: .env
volumes:
- phpvms_assets:/var/www/html/public/build:rw
- ./modules:/var/www/html/modules:ro
- phpvms_uploads:/var/www/html/public/uploads:rw
- phpvms_storage:/var/www/html/storage:rw
depends_on:
- caddy
- mariadb
- redis
networks:
- phpvms
task:
container_name: phpvms-task
user: "${WWWUSER:-1000}:${WWWGROUP:-1000}"
image: ghcr.io/phpvms/phpvms:main
restart: unless-stopped
command: ["php", "/var/www/html/artisan", "schedule:work"]
environment:
PHP_OPCACHE_ENABLE: 1
healthcheck:
# This is our native healthcheck script for the scheduler
test: ["CMD", "healthcheck-schedule"]
start_period: 10s
env_file: .env
volumes:
- ./modules:/var/www/html/modules:ro
- phpvms_uploads:/var/www/html/public/uploads:rw
- phpvms_storage:/var/www/html/storage:rw
networks:
- phpvms
queue:
container_name: phpvms-queue
user: "${WWWUSER:-1000}:${WWWGROUP:-1000}"
image: ghcr.io/phpvms/phpvms:main
restart: unless-stopped
command: ["php", "/var/www/html/artisan", "queue:work", "--tries=3"]
environment:
PHP_OPCACHE_ENABLE: 1
healthcheck:
# This is our native healthcheck script for the queue
test: ["CMD", "healthcheck-queue"]
start_period: 10s
env_file: .env
volumes:
- ./modules:/var/www/html/modules:ro
- phpvms_uploads:/var/www/html/public/uploads:rw
- phpvms_storage:/var/www/html/storage:rw
networks:
- phpvms
caddy:
container_name: phpvms-caddy
image: caddy:2
restart: unless-stopped
env_file: .env
ports:
- "${FORWARD_HTTP_PORT:-80}:80"
- "${FORWARD_HTTPS_PORT:-443}:443"
volumes:
- ./public/:/var/www/html/public
- phpvms_assets:/var/www/html/public/build
- ./resources/docker/Caddyfile:/etc/caddy/Caddyfile
- caddy_config:/config
- caddy_data:/data
networks:
- default
- phpvms
mariadb:
container_name: phpvms-mariadb
image: mariadb:11
restart: unless-stopped
# If someone need to access db from the outside
ports:
- "${FORWARD_DB_PORT:-3306}:3306"
environment:
MYSQL_ROOT_PASSWORD: "${DB_PASSWORD}"
MYSQL_DATABASE: "${DB_DATABASE}"
MYSQL_USER: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
MYSQL_ALLOW_EMPTY_PASSWORD: "no"
volumes:
- mariadb:/var/lib/mysql
networks:
- phpvms
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 10s
interval: 10s
timeout: 5s
retries: 3
redis:
container_name: phpvms-redis
image: redis:alpine
restart: unless-stopped
volumes:
- redis:/data
networks:
- phpvms
healthcheck:
test: ["CMD", "redis-cli", "ping"]
retries: 3
timeout: 5s
volumes:
caddy_data:
caddy_config:
mariadb:
phpvms_storage:
phpvms_uploads:
phpvms_assets:
redis:
networks:
phpvms:
driver: bridge

View File

@ -1,64 +0,0 @@
<code_scheme name="phpVMS" version="173">
<PHPCodeStyleSettings>
<option name="ALIGN_KEY_VALUE_PAIRS" value="true" />
<option name="ALIGN_PHPDOC_PARAM_NAMES" value="true" />
<option name="ALIGN_PHPDOC_COMMENTS" value="true" />
<option name="CONCAT_SPACES" value="false" />
<option name="COMMA_AFTER_LAST_ARRAY_ELEMENT" value="true" />
<option name="PHPDOC_BLANK_LINE_BEFORE_TAGS" value="true" />
<option name="PHPDOC_BLANK_LINES_AROUND_PARAMETERS" value="true" />
<option name="PHPDOC_WRAP_LONG_LINES" value="true" />
<option name="THROWS_WEIGHT" value="1" />
<option name="RETURN_WEIGHT" value="2" />
<option name="LOWER_CASE_BOOLEAN_CONST" value="true" />
<option name="LOWER_CASE_NULL_CONST" value="true" />
<option name="ELSE_IF_STYLE" value="COMBINE" />
<option name="FIELDS_DEFAULT_VISIBILITY" value="protected" />
<option name="VARIABLE_NAMING_STYLE" value="CAMEL_CASE" />
<option name="BLANK_LINES_BEFORE_RETURN_STATEMENT" value="1" />
<option name="KEEP_RPAREN_AND_LBRACE_ON_ONE_LINE" value="true" />
<option name="SPACES_WITHIN_SHORT_ECHO_TAGS" value="false" />
<option name="FORCE_SHORT_DECLARATION_ARRAY_STYLE" value="true" />
<option name="PHPDOC_USE_FQCN" value="true" />
</PHPCodeStyleSettings>
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="JavaScript">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="PHP">
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="KEEP_LINE_BREAKS" value="false" />
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="0" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="BLANK_LINES_AFTER_PACKAGE" value="1" />
<option name="FINALLY_ON_NEW_LINE" value="true" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_EXTENDS_LIST" value="true" />
<option name="ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION" value="true" />
<option name="SPACE_AFTER_TYPE_CAST" value="true" />
<option name="SPACE_BEFORE_IF_PARENTHESES" value="false" />
<option name="SPACE_BEFORE_CLASS_LBRACE" value="false" />
<option name="METHOD_PARAMETERS_WRAP" value="5" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="ARRAY_INITIALIZER_WRAP" value="5" />
<option name="ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE" value="true" />
<option name="ARRAY_INITIALIZER_RBRACE_ON_NEXT_LINE" value="true" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<option name="WRAP_ON_TYPING" value="0" />
<option name="SOFT_MARGINS" value="100" />
</codeStyleSettings>
</code_scheme>

View File

@ -1,94 +0,0 @@
# phpVMS task runner
#
# Install just: brew install just (or cargo install just)
# List recipes: just --list
set shell := ["bash", "-cu"]
# Default: list all recipes
default:
@just --list
# ── Frontend ────────────────────────────────────────────────────────────────
# Vite dev server (theme + admin map bundle, HMR)
dev:
npm run dev
# Watch Alpine components (esbuild --dev, inline sourcemaps)
dev-components:
npm run dev:components
# Production build: Vite (theme + admin) + esbuild (Alpine components) + Filament asset publish
build:
npm run build
php artisan filament:assets
# Vite only — theme + admin map bundle
build-vite:
npm run build
# esbuild only — Filament AlpineComponent bundles
build-components:
npm run build:components
php artisan filament:assets
# Publish Filament assets into /public (AlpineComponents, plugin CSS/JS)
publish:
php artisan filament:assets
# ── Linting / Formatting ────────────────────────────────────────────────────
# JS lint (oxlint)
lint-js:
npm run lint
lint-js-fix:
npm run lint:fix
# JS format (oxfmt)
fmt-js:
npm run fmt
fmt-js-check:
npm run fmt:check
# PHP format (Pint, dirty files only)
fmt-php:
vendor/bin/pint --dirty
fmt-php-check:
vendor/bin/pint --test
# ── Static Analysis ─────────────────────────────────────────────────────────
# PHPStan / Larastan (level 5)
stan:
vendor/bin/phpstan analyse
# Rector dry-run (no writes)
rector-dry:
vendor/bin/rector --dry-run
# Rector apply (writes changes)
rector:
vendor/bin/rector
# ── Testing ─────────────────────────────────────────────────────────────────
# Full Pest suite
test:
composer test
# Filter tests: `just test-filter PirepTest`
test-filter filter:
php artisan test --compact --filter={{filter}}
# ── Pre-PR Checklist (matches AGENTS.md) ────────────────────────────────────
# Run all four gates: pint, phpstan, pest, rector
check:
composer pint --test
vendor/bin/phpstan analyse
composer test
vendor/bin/rector --dry-run

8
mise.toml Normal file
View File

@ -0,0 +1,8 @@
[env]
BUILDKIT_HOST = "docker-container://buildkit"
[tasks.setup]
run = ["mise install", "mise run run-buildkit-container || true"]
[tasks.run-buildkit-container]
run = "docker run --rm --privileged -d --name buildkit -e BUILDKIT_DEBUG=1 moby/buildkit:latest"

View File

@ -2,17 +2,17 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"dev:components": "node bin/build.js --dev",
"build": "vite build && node bin/build.js",
"build:components": "node bin/build.js",
"dev": "bun x --bun vite",
"dev:components": "bun bin/build.js --dev",
"build": "bun x --bun vite build && bun bin/build.js",
"build:components": "bun bin/build.js",
"clean": "rm -rf public/build resources/js/dist",
"lint": "oxlint",
"lint:fix": "oxlint --fix",
"fmt": "oxfmt",
"fmt:check": "oxfmt --check",
"typecheck": "tsc --noEmit",
"test": "vitest run"
"test": "bun x --bun vitest run"
},
"dependencies": {
"@date-fns/tz": "^1.5.0",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
railpack.json Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "https://schema.railpack.com",
"deploy": {
"startCommand": "php artisan octane:start --server=frankenphp --host=0.0.0.0 --port=${FORWARD_HTTP_PORT:-8080}"
}
}

View File

@ -1,9 +0,0 @@
{$CADDY_DOMAIN} {
root * /var/www/html/public
encode zstd gzip
file_server
php_fastcgi app:9000
}

View File

@ -1,4 +0,0 @@
#!/bin/sh
rm -rf ./public/build/*
cp -R ./build/* ./public/build
rm -rf ./build

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
use App\Http\Middleware\DisableActivityLoggingByDefault;
use App\Http\Middleware\EnableActivityLogging;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Spatie\Activitylog\ActivityLogStatus;
/**
* Octane keeps the framework booted across requests on the same worker.
* The DisableActivityLoggingByDefault middleware exists to reset logging
* to "off" at the start of every request, so an earlier request's
* EnableActivityLogging call does not leak into the next request.
*
* @group octane
*/
pest()->group('octane');
test('disable middleware turns activity logging off at request start', function (): void {
activity()->enableLogging();
expect(app(ActivityLogStatus::class)->disabled())->toBeFalse();
(new DisableActivityLoggingByDefault())->handle(Request::create('/'), fn (): Response => new Response());
expect(app(ActivityLogStatus::class)->disabled())->toBeTrue();
});
test('logging from request A does not leak into request B on the same worker', function (): void {
$disable = new DisableActivityLoggingByDefault();
$enable = new EnableActivityLogging();
// Request A — enabled via EnableActivityLogging.
$disable->handle(Request::create('/admin'), fn (): Response => $enable->handle(Request::create('/admin'), function (): Response {
expect(app(ActivityLogStatus::class)->disabled())->toBeFalse();
return new Response();
}));
// Request B — same booted application, no EnableActivityLogging this time.
$disable->handle(Request::create('/'), function (): Response {
expect(app(ActivityLogStatus::class)->disabled())->toBeTrue();
return new Response();
});
});

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
use App\Services\ModuleService;
/**
* ModuleService used to hold its admin/frontend link arrays as `protected
* static array`. Static state survives across requests on the same Octane
* worker, which is fine when registration happens once at module boot but
* was a footgun for any future per-request caller. The arrays are now
* instance state on a shared singleton; this test asserts that fetching
* the link list multiple times in a row does not grow it.
*
* @group octane
*/
pest()->group('octane');
test('admin link count is stable across repeated reads', function (): void {
/** @var ModuleService $service */
$service = app(ModuleService::class);
$service->addAdminLink('Sample', '/admin/sample', 'pe-7s-note');
$baseline = count($service->getAdminLinks());
foreach (range(1, 5) as $_) {
expect(count($service->getAdminLinks()))->toBe($baseline);
}
});
test('frontend link count is stable across repeated reads', function (): void {
/** @var ModuleService $service */
$service = app(ModuleService::class);
$service->addFrontendLink('Sample', '/sample', 'bi bi-people', true);
$baseline = count($service->getFrontendLinks(true));
foreach (range(1, 5) as $_) {
expect(count($service->getFrontendLinks(true)))->toBe($baseline);
}
});

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
use App\Http\Middleware\SetActiveTheme;
use Igaster\LaravelTheme\Facades\Theme;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
/**
* SetActiveTheme used to early-return on admin/api/install paths, leaving
* whatever theme the previous frontend request had set on the long-lived
* Theme singleton. Under Octane that's a per-worker leak. The middleware
* now always sets a baseline theme.
*
* @group octane
*/
pest()->group('octane');
test('admin path does not inherit theme from a prior frontend request', function (): void {
updateSetting('general.theme', 'seven');
$middleware = new SetActiveTheme();
// Request 1: frontend route — middleware sets the configured theme.
$middleware->handle(Request::create('/'), fn (): Response => new Response());
$themeAfterFrontend = Theme::get();
// Request 2: admin route — same booted Theme singleton.
$middleware->handle(Request::create('/admin'), fn (): Response => new Response());
$themeAfterAdmin = Theme::get();
// Both should equal the configured default. The bug we're guarding
// against was: request 2 inherits request 1's theme implicitly because
// the middleware short-circuits on /admin and never resets the singleton.
expect($themeAfterAdmin)->toBe($themeAfterFrontend);
});

View File

@ -1,26 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"version": "8.0-alpha.{height}",
"publicReleaseRefSpec": ["^refs/heads/main$", "^refs/heads/releases/v\\d+(?:\\.\\d+)?$"],
"release": {
"branchName": "releases/v{version}",
"versionIncrement": "build",
"firstUnstableTag": "alpha"
},
"cloudBuild": {
"buildNumber": {
"enabled": true
},
"setVersionVariables": true,
"setAllVariables": true
},
"pathFilters": [
":!/.config",
":!/.github",
":!/build",
":!/modules",
":!/storage",
":!/tests",
":!/version.json"
]
}