The setting() memo is request-scoped under Octane via config/octane.php 'flush',
but a long-running queue worker is not flushed per job. Reset the memo in a
Queue::before hook so a worker observes settings changed by other processes;
otherwise, after an external store() busts the shared cache, the worker's next
Cache::remember() miss would re-read the stale memoized value and write it back,
poisoning the cache. Also corrects the clearMemo() docblock.
Review follow-ups on the with=bid fast-path:
- Resolve all returned flights' bids in one batched Bid::whereIn query instead
of one query per flight in the search() loop (removes a reintroduced N+1).
- Eager-load bid.aircraft.subfleet.aircraft (SubfleetResource reads ->aircraft):
matches the legacy accessible-fleet eager set, so serializing >=2 bid subfleets
no longer lazy-loads (a LazyLoadingViolationException / 500 outside production).
- Store subfleets as an Eloquent Collection (not the Support Collection from
pluck()) so the relation's type contract holds.
- Trim `with` tokens in search() to match hasBidToken().
Adds a multi-bid serialization test and a batched-query-count assertion.
SettingService gains an in-instance memo so repeated setting() reads of the same
key resolve the source (Redis/DB) at most once per request. Bound as a singleton
and added to config/octane.php 'flush' so the memo lifetime is exactly one
request. store()/save() evict the key alongside forgetCache(); YamlDatabaseService
clears the memo after raw settings writes so same-request reads stay coherent.
Collapses the ~1,900-read setting() N+1 seen when serializing a large fleet.
Add a controlled `bid` token to the flights get()/search() `with=` param.
When present, resolve the authenticated pilot's bid(s) on the flight, load
`bid.aircraft.subfleet.fares`, set `subfleets` to just those subfleet(s), and
skip accessibleSubfleetsFor entirely. No bid -> empty subfleets. `bid` is a
server-controlled whitelist token, never passed to Eloquent ->with().
search() also passes onlyActive = !filled('flight_id') so a keyed by-id lookup
returns the flight regardless of `visible` (parity with get()); browse search
still applies visible().
Cuts a bid briefing from expanding the whole accessible fleet (~234 subfleets on
the live VA) to ~5 indexed queries.
fix(finance): keep the journal relation fresh after initJournal()
The `!$this->journal` guard in JournalTrait::initJournal() lazy-loads the
journal relation and caches it as null. Saving the newly created journal
through the relation does not update that cache, so the creating model kept
returning null for ->journal until it was reloaded from the database.
Set the relation on the instance after saving, and add a regression test
asserting the journal is available immediately after creation.
* fix: change the plugin directory name to <author>-<name> (from registry_id)
* lower-case the dir name
* Fix test
* update the keyed name (x-y)
* cleanup the unused code
* More cleanup
fix(addons): use standard Laravel database/ layout for module migrations and seeders
The installer's MigrationService and SeederService discovered addon
resources under a capitalized `Database/migrations` and `Database/seeds`
path, but the base module ServiceProvider (and standard Laravel) use
lowercase `database/migrations` and `database/seeders`. On a
case-sensitive filesystem the installer never found a module's lowercase
directories, so migrations were skipped in the installer flow and
seeders never ran.
Standardize discovery on the lowercase, standard-Laravel layout:
- SeederService scans `database/seeders`
- MigrationService scans `database/migrations`
- align the Sample scaffold directories and its provider
- update the addon migration/seeder feature-test fixtures
Co-authored-by: Nabeel S. <99736+nabeelio@users.noreply.github.com>
* refactor(settings): extract typed setting casting into a shared trait
Move the bool/date/int/float/string casting match out of SettingService into a reusable CastsSettingValue trait so the upcoming addon settings service casts identically.
* feat(addons): add per-addon settings management
Addons declare settings via a HasSettings provider method; they are synced
into a dedicated addon_settings table on boot (preserving user values) and
read/written through addon_setting()/addon_setting_save() by alias or
registry_id. A shared AddonSettings Filament page is inherited by each addon
panel, scoped to that addon. Editing requires module access plus the new
edit:addon-settings permission. Sample module ships a reference schema.
* fix(addons): address review feedback on addon settings
- canAccess() excludes hidden settings so an addon with only hidden rows
no longer shows an empty settings page
- dedupe normalized keys before upsert and always log orphans, even when an
addon now declares no settings
- addon_setting() catches only SettingNotFound so unexpected errors surface
- log swallowed boot-time sync failures for observability
- add explicit types to sample_setting(); authenticate the page-access test
* fix(pireps): clear stale ACARS track and logs when reusing a duplicate prefile
When a flight didn't close out (PIREP left IN_PROGRESS after a divert or
abort), restarting the same flight within the duplicate window made
prefile() adopt the old PIREP wholesale, keeping its FLIGHT_PATH rows
(a stray inbound line into the new departure) and LOG rows (a fused
flight log). Clear the reused leg's track and logs so the restarted
flight starts clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(pireps): assert planned ROUTE survives the duplicate-reuse cleanup
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The base module ServiceProvider auto-wired config, routes, views,
translations, commands and listeners but never registered migrations, so
an enabled module's migrations were skipped by artisan migrate.
Load migrations from {root}/database/migrations, matching the module
app-layout convention. Add coverage proving the path is registered with
the migrator and that migrate creates the module table.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Addon database migrations are now automatically registered and loaded
from addon-specific migration directories, eliminating manual setup
requirements and streamlining addon development. This enables addon
creators to focus on functionality rather than infrastructure
configuration.
* **Tests**
* Feature tests were added to validate the automatic migration
registration system and verify migrations execute properly.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
## Release Notes
* **New Features**
* Added role & permission management UI with a matrix-based permission
picker.
* Introduced `permission:sync` to populate permissions from the registry
(with optional pruning).
* Added `permission:generate-policies` to create missing policy stubs on
demand.
* **Changes**
* Switched authorization to a permissions-based system with super-admin
bypass and consistent ability naming (including updated `edit:flight`
access).
* Updated Filament page and menu access gating to match the new
permission keys.
* Removed Filament Shield usage; streamlined policy authorization
behavior across resources and pages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
canAccessPanel() short-circuited on module panels and never fell through to the legacy view:modules permission, so a user holding only view:modules was denied every module panel despite the documented fallback.
Move normalizePath()/isWithin() out of ManifestParser into a shared
App\Support\Filesystem so the same traversal-safe boundary check can be
reused. Apply it to the ZipSource zip-slip post-extraction guard, per
review feedback.
Defers module route registration to the end of the routing stack. This
fixes the issue where core routes were matching first and blocking
module-level overrides.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Chores**
* Deferred loading of addon route files until the application completes
booting, improving startup performance and ensuring routes are
registered at the right lifecycle stage.
* Refreshed route name and action lookups after addon route registration
to keep route resolution consistent.
* **Tests**
* Updated addon service provider test setup to rely on the new deferred
boot behavior, removing now-unnecessary manual route lookup refresh
calls.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Configure ->login() on the module panel base so unauthenticated
access has a defined redirect target.
- Use the named frontend.home route for the panel 'go back' item
instead of a hardcoded '/'.
- Switch the panel switcher links to $panel->getUrl() (respecting
panel routing/domain), falling back to the path-based URL.
- Use firstOrCreate for permissions/roles in the Sample panel test so
it is deterministic when seeders already inserted them.
- Drop two inline comments flagged as non-complex.
realpath() returns false for non-existent targets, so an escaping
autoload.files or psr-4 entry (e.g. ../secret.php) whose file did not
yet exist skipped the boundary check and was cached. Normalise paths
lexically instead so traversal is rejected regardless of existence.
Modules previously had their Filament resources injected into the main
admin panel and surfaced legacy addAdminLink() items as sidebar nav.
Invert this: each module owns a dedicated Filament panel and a navbar
panel switcher moves between the main panel and module panels.
- Add App\Contracts\Modules\PanelProvider: an abstract base that
pre-configures id (= module key), path (admin/{key}), middleware,
auth, branding, the shared admin theme/sidebar (viteTheme +
sidebarWidth 14.5rem for visual consistency), a Dashboard page, the
shared plugins, and discovery of the module's own
Filament/{Resources,Pages,Widgets}. Modules register it via their
providers list and override only moduleKey().
- Add PanelSwitcherPlugin: a topbar-left dropdown listing the main
panel plus every module panel the user can access, on every panel.
- Stop injecting module components into the core admin/system panels:
remove FilamentPanelExtender, its beforeResolving('filament') hook,
probeFilament(), and the boot-cache filament field (schema bump).
- Remove the legacy link APIs (addAdminLink/addFrontendLink/
registerLinks) and their blade/view consumers.
- Gate module panels via access:{module-key} in canAccessPanel(),
with the view:modules fallback.
- Migrate the Sample module to ship its own panel provider and drop
its old admin controller/route.
The addon engine registered each enabled addon's PSR-4 namespace and
service providers but never loaded composer autoload.files, so module
global helpers (e.g. a helpers.php) were silently unavailable.
Thread autoload.files through ManifestParser -> AddonManifest ->
AddonBootCache -> AddonAutoLoader, requiring each file once per boot
cycle (after PSR-4, before providers). Bump the boot-cache schema so
pre-change caches rebuild. Add a Sample module helpers.php as the
reference pattern.
Addon routes load from a deferred booted() callback so they register
after core's routes. That also runs after the framework's own route
name/action lookup refresh, leaving addon routes unindexed and breaking
route() and Route::has() discovery for them.
Refresh the name/action lookups after loading, mirroring Laravel's
RouteServiceProvider. Drop the test's manual refreshNameLookups()
workaround so the existing Route::has() assertion covers the fix.
Defers module route registration to the end of the routing stack. This
fixes the issue where core routes were matching first and blocking
module-level overrides.
Make the three auto-price override columns cast the setting to bool before
negating, matching the price column above. Behavior is unchanged (boolean
settings already resolve to a real bool), but the intent is now explicit.
Add base_price/per_nm/multiplier to the fare form, the subfleet fares
relation manager (toggleable, shown when auto pricing is on and hidden
otherwise, with the static price column inverted), and the low_cost toggle
to the airline form. Round-trip the new columns through the fare
importers/exporters.
Add FareService::getAutoPrice() — (base + distance_nm * per_nm) * multiplier
* low_cost_factor, clamped to >= 0 — and apply it in saveToPirep() when
fares.auto_price is enabled. Distance is the great-circle nm between the
PIREP airports (new GeoService::airportDistance()), falling back to the
PIREP's stored distance, so it works for scheduled and free flights.
Seed fares.auto_price (opt-in toggle, default off) and
fares.low_cost_multiplier, with an idempotent data migration for existing
installs. Render float-typed settings as numeric inputs on the Settings page.
Add base_price, per_nm and multiplier to fares (overridable per subfleet
via the subfleet_fare pivot) and a low_cost flag to airlines, with model
fillable/casts and factory defaults.
- Gate all RouteForge layers on edit:flight (route middleware, controller
docblock, JS comment, test names) to match the page gate and can_commit
- Scope the permission:sync --prune query by guard_name so it cannot delete
other guards' permissions
- Throw when the super-admin role is missing before legacy group import
instead of dereferencing null
- Guard BasePolicy against an empty $subject to avoid malformed permission
names
- Drop the redundant is_string() check on Panel::getPages() (always true)
- Add coverage for BasePolicy, the super-admin gate, PermissionRegistry,
the roles matrix resource, page authorization, module panel access,
and the sync/generate-policies commands
- Migrate existing tests off ShieldSeeder onto RolesPermissionsSeeder
- Invoke the commands by their real singular names (permission:sync,
permission:generate-policies)
- Replace ShieldSeeder with RolesPermissionsSeeder, which creates the
super-admin role and runs `permission:sync` to build the catalog
- Sync permissions during installer seeding and the legacy role import
- Resolve the super-admin role by Role::superAdminName() in the
notifications subscriber and legacy group importer
- `permission:sync` persists the registry's permission catalog into the
database (with an optional `--prune`) and flushes the spatie cache
- `permission:generate-policies` scaffolds thin BasePolicy subclasses for
Filament resource models
- Add the Roles resource with a grouped permission matrix sourced from
the PermissionRegistry (resource/page/module/custom permissions)
- Authorize pages and widgets via the AuthorizesAccess concern instead
of filament-shield traits; drop HasWidgetShield and the
FilamentShieldPlugin registration
- Gate RouteForge on `edit:flight` and hide the user roles field behind
Role::superAdminName()
- Add the supporting translation strings
Reduce every Filament policy to a `$subject` slug and delegate all
methods to BasePolicy, which maps them onto view/edit/delete abilities.
Replaces the per-method filament-shield checks. Adds FlightBundlePolicy
and a Sample module policy to cover the new resources.
- Register a `Gate::before` hook so the super-admin role bypasses every
permission check, replacing the removed filament-shield gate
- Add Role::superAdminName() as the single source for the bypass role
- Gate module panel access behind per-module `access:<module>`
permissions via the PermissionRegistry, falling back to `view:modules`
- Bind PermissionRegistry as a singleton
- Point config/permission.php at the app's Role and Permission models
Introduce the foundation that replaces filament-shield:
- Ability enum (view/edit/delete) mapping policy methods to permission
names of the form `{ability}:{subject}`
- BasePolicy that resolves every Filament policy method onto an ability
for a declared subject slug
- PermissionRegistry as the single source of truth for resource, page,
module and custom permissions
- ProvidesPermissions contract plus the HasPermissionKey and
AuthorizesAccess Filament concerns
- config/roles.php for the super-admin role name, guard and custom
permission catalog
- Removed legacy sidebar rendering for module links in favor of native Filament sidebar items.
- Backfilled `addons.name` column using data from `module.json` or fallback derivation logic.
- Enhanced compatibility for Octane by ensuring idempotent handling of legacy admin links.
- Introduced `addon_vite` helper for rendering Vite tags from addon manifests.
- Updated `AddonAssetLinker` to ensure consistent handling of lower-cased paths for addon symlinks.
- Added extensive unit and feature tests for addon navigation, asset linking, and legacy handling.
- Updated configuration and boot cache logic to support test isolation and improved flexibility.
- Introduced `AddonSource` interface and concrete `UrlSource` and `ZipSource` implementations for handling addon payloads.
- Added `AddonValidator` for extracting and validating addon manifests and autoload paths.
- Implemented `install` and `update` methods in `AddonRegistry` to support lifecycle management.
- Integrated Octane worker reloading functionality via `OctaneReloader`.
- Added unit and feature tests for installation, update handling, validation, and new behavior.
- Added `AddonAssetLinker` to manage public asset symlinks for addons.
- Introduced the `addons:relink` Artisan command to rebuild asset links for enabled addons.
- Updated `AddonRegistry` to support asset linking/unlinking functionality.
- Adjusted configuration to set `addons.paths.assets` to `public/ext`.
- Added unit and feature tests for `AddonAssetLinker` and the new command.
- Removed `Module` and `ModuleRepository` compatibility shims.
- Replaced NWIDART `Module` facade usages with the new `AddonRegistry`.
- Updated frontend controllers to use `AddonRegistry` for module handling.
- Added new feature tests to cover `AddonRegistry` lifecycle and rebuild cases.
- Updated bootstrapping logic and service bindings to drop NWIDART dependencies.
# Conflicts:
# app/Models/Flight.php
# app/Support/Utils.php