phoenix1331/filament-jev-moderation
Filament plugin for JEV moderation that adds review queues, approval and rejection actions, and status tracking to your Laravel admin panel. Designed for teams that need clear, audit-friendly content moderation workflows, it streamlines oversight and keeps moderated items organized.
This package is a Filament panel UI over the phoenix1331/laravel-jev-moderation core (requires core 0.2). Almost nothing here does moderation itself — it reads the core's decisions, config and calibration data, and writes back through approve() / reject(). Install the core and get it producing decisions before touching this package.
phoenix1331/laravel-jev-moderation:^0.2 and this plugin.->plugin(
JevModerationPlugin::make()
->models([Post::class, Comment::class])
->modelLabels(['post' => 'Posts', 'comment' => 'Comments'])
->authorRelation('user')
)
authorize() before deploying anywhere non-local. Without a callback, access is local-environment-only and every page and widget 403s on mount and on each request. This is the single most common "it's broken in staging" cause.authorizeActionsUsing() or a registered ModerationDecision policy (Filament Shield works). Abilities are approve, reject, reclassify.The Overview widget (waiting count, oldest wait, blocks since midnight, median queue-to-review, 7-day override rate) is the best single check that the pipeline is alive.
Triage loop with the queue. The queue is sorted most-confident-first, then longest-waiting. Row actions are icon buttons pinned before the columns so they stay reachable beside an open sidebar at 1280px. Approve/reject accept a selection spanning multiple model types and an optional note (≤2,000 chars). Everything goes through the core, so your existing ReviewResolved listeners fire exactly as they do elsewhere — wire notifications, cache busts, or re-indexing there rather than into Filament action hooks.
Handle stale rows gracefully. A record changed since page load (already reviewed, re-reported, rescanned), deleted, or outside the moderator's action policy is skipped. The notification reports how many were skipped; a record the core refuses (e.g. its policy was removed from config) is reported with the reason without aborting the batch. Treat the queue as eventually-consistent and never assume the batch applied wholesale.
Reclassify instead of reject-with-wrong-category. When Jev flagged the right content under the wrong category, use reclassify: it records a core reject() under the corrected category, so your analytics and calibration stay honest. It only offers the record's other harmful categories from its policy, and disappears if the policy is gone from config.
Tuning is a config + rescan loop. Read the Thresholds page for each policy's review_above, block_above, uncertain_below, version and consuming models. Change config, then run the suggested php artisan jev-moderation:rescan per model, naming the status the change affects — flagged normally, blocked after raising block_above. Then watch the override rate fall in the calibration widget. Do not rescan a status you didn't change; that re-checks decisions moderators already made.
Embed widgets in your own dashboards. ModerationOverview and DecisionVolumeChart are ordinary widgets — add them to a non-moderation dashboard, they refresh every minute. CalibrationTable with onlyFlagged gives you just the bands needing attention; the thresholds page carries the full picture and the Jev version selector.
Deep links back to your resources. Each queued row has a "View" link to your own Filament view page (falls back to edit, respecting permissions, opens in a new tab, hidden when absent). Override with recordUrlUsing(fn ($record) => ...), or return null to hide it entirely — useful when your resources aren't ready for moderators or you want a custom preview.
Author-level investigation. Set authorRelation() (default user) or authorRelations([...]) per model so the author history page resolves. It aggregates every decision across the types that author writes, with 7/30-day per-status counts — the fastest way to spot a repeat offender or a miscalibrated policy hitting one verbose user.
authorize() must return exactly true. A truthy return is not enough; a callback that leaks a model instance or a non-empty array denies access. Return a strict boolean.modelLabels() takes the whole panel down, not just the moderation pages. Register the plugin last and test after edits.models([...]) if you want the queue curated.includeBlocked() is off by default. Auto-blocks are not in the queue until you opt in — expect "where did the blocks go?"overrideWarningRate() and have at least minimumSample() reviews before it is flagged — with a fresh install you will see no warnings for a while. That is deliberate, not a bug.php artisan jev-moderation:stats does; if the numbers diverge, you have a duplicate/legacy review path, not a UI bug.config/jev-moderation.php against deployed config.How can I help you explore Laravel packages today?