Native plugins developer guide
This document describes the native plugin system introduced alongside the registry-based handlers (image_webhook, screenshot, …). It complements recipe plugins (plugin_type = recipe), which use Liquid/Blade markup, polling, and the existing recipe editor.
Concepts
| Piece | Role |
|---|---|
PluginHandler |
Abstract contract: metadata, optional webhook handling, optional produce() for the display pipeline, configuration UI hooks. |
PluginRegistry |
Singleton map of key() → handler instance. Built in PluginServiceProvider. |
Plugin model |
plugin_type stores the handler key. Plugin::handler() resolves the handler from the registry (returns null for unregistered types such as recipe). |
PluginOutput |
Tells GenerateScreenJob how far the plugin’s asset is through the e-paper pipeline. |
PluginContent |
Immutable result type for produce() (HTML bytes, raw image, or processed image UUID). Not all plugins use produce() yet. |
PluginActionController |
Single HTTP entrypoint for plugin webhooks; delegates to the handler for the bound plugin’s plugin_type. |
Legacy PluginImageWebhookController was removed; the same behavior is handled generically by PluginActionController.
Registration
- Implement a concrete class extending
App\Plugins\PluginHandler. - Register it in
App\Providers\PluginServiceProviderinside thePluginRegistrysingleton closure.
Handlers must return a stable string from key(); that value is persisted on plugins.plugin_type and used for routes, webhooks, and the job pipeline.
PluginHandler API (summary)
Implement or override as needed:
key(),name(),description(),icon()— Shown on the plugins index and type listing (flux:icon name).output(): PluginOutput— DefaultHtml. DrivesGenerateScreenJob(see below).defaultAttributes(): array— Merged intoPlugin::create()for new instances.fields(): array— Schema for the shared type instance Livewire page (plugins.type-instance). Each field is persisted underPlugin::$configuration['key'].settingsPartial(): ?string— Optional Blade view (e.g.plugins.image-webhook.settings) appended to the instance page; receives$plugin.isDataStale(Plugin $plugin): ?bool— Returnnullto use the model’s default stale logic; return a boolean to override.handleWebhook(Request, Plugin): JsonResponse|array— Called for HTTP webhooks. Default is 404 JSON.produce(Plugin, Device|DeviceModel|null): PluginContent— Intended for display generation; default throws. Override when the pipeline should call into the handler (see Pipeline).
hasInstances() is currently true for all handlers; the plugins index only lists registry entries that pass this check (plus static “Markup” / “API” doc tiles).
Display pipeline and PluginOutput
App\Jobs\GenerateScreenJob inspects $plugin->handler()?->output():
PluginOutput |
Meaning in the job |
|---|---|
ProcessedImage |
The plugin already stores a device-ready file under images/generated/{uuid}.{ext} on the public disk. The job copies plugins.current_image to the device’s current_screen_image and skips Browser/Image pipeline stages. |
Image |
Same BrowserStage + ImageStage path as HTML recipes, but the handler’s configureBrowserStage() hook binds the stage (e.g. screenshot calls BrowserStage::url() from Plugin::$configuration instead of html()). GenerateScreenJob uses ImageGenerationService::generateImage(..., $plugin). |
Html (default) |
Markup is run through ImageGenerationService::generateImage() (full BrowserStage + ImageStage pipeline), then plugins.current_image / metadata are updated as today for recipes. |
Image webhook sets output() to ProcessedImage and updates current_image inside handleWebhook() when a client POSTs an image; it does not rely on produce().
Screenshot declares Image, defines url in fields(), and overrides configureBrowserStage() to read that URL. Per-device cache invalidation uses current_image_metadata like recipes (ImageGenerationService::resetIfNotCacheable).
Webhook HTTP API
Public routes (UUID route-model binding on Plugin):
| Method | Path | Name | Notes |
|---|---|---|---|
POST |
/api/plugins/{plugin:uuid}/webhook |
api.plugins.webhook |
Preferred. |
POST |
/api/plugin_settings/{plugin:uuid}/image |
api.plugin_settings.image |
Backward compatibility for older image-webhook clients; same controller. |
PluginActionController loads $registry->get($plugin->plugin_type) and returns 400 if no handler is registered (e.g. a recipe row hit by mistake).
Image webhook behavior
Handler key: image_webhook (ImageWebhookPlugin::KEY).
- Accepts uploads via
App\Actions\Api\ProcessPluginImageUpload(multipartimage, raw body withimage/pngorimage/bmp, base64 / data URI, etc.—seetests/Feature/Api/ImageWebhookTest.php). - Persists PNG or BMP only to
storage/app/public/images/generated/{uuid}.{ext}and setsplugins.current_imageto that UUID. - Triggers
ImageGenerationService::cleanupFolder()after success.
UI routes (authenticated)
From routes/web.php:
plugins.index— Lists doc tiles + registered native types + user recipe plugins (native instances are managed per type).plugins.type/plugins.type-instance—{type}is the handlerkey(); instance page usesfields(),settingsPartial(), and shared Livewire views underresources/views/livewire/plugins/.
Adding a new native plugin (checklist)
- Add
app/Plugins/YourPlugin.phpextendingPluginHandlerwith a uniquekey()(e.g.my_feed). - Register
new YourPlugininPluginServiceProvider. - Decide
PluginOutputand implementhandleWebhookand/orproduce()+PluginContentfactories as the feature requires. - If users need form fields, implement
fields()and/orsettingsPartial()Blade underresources/views/plugins/.... - Add a factory state or migration path so new
Pluginrows getplugin_type= your key (and anydefaultAttributes()). - Add Pest coverage under
tests/Feature/Plugins/and/ortests/Feature/Api/for webhooks and pages.
Related tests
tests/Feature/Plugins/PluginRegistryTest.php— Registry wiring.tests/Feature/Plugins/TypeInstancePageTest.php— Type / instance Livewire pages.tests/Feature/Api/ImageWebhookTest.php— Webhook upload formats and BC route.
File reference (current tree)
| File | Purpose |
|---|---|
app/Plugins/PluginHandler.php |
Contract |
app/Plugins/PluginRegistry.php |
Registry |
app/Plugins/PluginContent.php |
produce() result |
app/Plugins/Enums/PluginOutput.php |
Pipeline stage enum |
app/Plugins/ImageWebhookPlugin.php |
Processed-image webhook plugin |
app/Plugins/ScreenshotPlugin.php |
Screenshot URL → BrowserStage + PluginOutput::Image |
app/Providers/PluginServiceProvider.php |
Registers handlers |
app/Http/Controllers/Api/PluginActionController.php |
Generic webhook controller |
routes/api.php |
Webhook routes |
app/Jobs/GenerateScreenJob.php |
Branching on PluginOutput |