# Debugging plugins

> Find out why a FeatherPanel plugin does not load, a route returns 404 or a page stays empty. Log messages, the validate endpoint, worker restarts and a release checklist.

Source: https://featherpanel.com/docs/plugins/debugging/

## Where to look

| Tool | What it tells you |
| --- | --- |
| `storage/logs/App.fplog` | Everything the plugin loader, the router and your own logger calls write. In Docker: `/var/www/html/storage/logs/App.fplog` in the backend container, on the `featherpanel_logs` volume. |
| **Logs** in the admin area (System group) | The same log in the browser. Needs `admin.root`. |
| **Admin > Plugins > your plugin** | Whether the plugin is loaded, unmet dependencies and missing required settings. |
| `GET /api/admin/plugin-manager/{identifier}/validate` | A checklist, see below. Developer mode and `admin.root` only. |
| `/var/log/cron.log` in the backend container | Output of the cron runner, including your cron classes. |
| Browser dev tools | Errors from `Frontend/index.js`, your iframe pages and failed `/api/addons/...` calls. |

Write your own log lines with the panel logger:

```php
use App\App;

$logger = App::getInstance(true)->getLogger();
$logger->info('myplugin: sync started');
$logger->warning('myplugin: remote API slow');
$logger->error('myplugin: sync failed: ' . $e->getMessage(), false);
```

`error()` and `critical()` also send the message to the panel's error telemetry unless the second argument is
`false`. `debug()` only writes when `APP_DEBUG` is on, which it is not in release builds, so use `info()` instead.

## Developer mode and the debug console

Turn on developer mode (the `app_developer_mode` setting) in the admin settings. It unlocks:

- **Admin > Dev > Plugins**: create plugins from templates, edit their manifest and settings, and add migrations,
  cron jobs, commands and public files with **Create file**,
- plugin export,
- the validate endpoint,
- the panel debug console: type `##icanhasfeatherpanel` in the global search. It has network probes, benchmarks, a
  log tail for admins, a debug snapshot you can copy, and links to the developer docs and the dev plugin page.

## The validate endpoint

`GET /api/admin/plugin-manager/{identifier}/validate` returns:

| Field | True when |
| --- | --- |
| `config_valid` | `conf.yml` passes the same checks the loader uses. |
| `dependencies_met` | Every `dependencies` entry is satisfied. |
| `required_configs_set` | Every key in `requiredConfigs` has a stored value. |
| `identifier_valid` | The identifier only uses letters, digits and `_`. |
| `flags_valid` | Every flag is a known flag. |
| `files_exist` | `conf.yml` and `<plugin.name>.php` exist in the plugin root. |
| `overall_valid` | All of the above. |

## Log messages from the plugin loader

The loader checks each folder in `storage/addons/` in this order and stops at the first problem:

| Log message | Cause and fix |
| --- | --- |
| `Invalid plugin identifier: <folder>` | The folder name has characters other than letters, digits and `_`. Rename it. |
| `Plugin config is empty for: <id>` | `conf.yml` is missing, empty or does not parse. |
| `Missing key for plugin: <id> key: <key>` then `Invalid config for plugin: <id>` | A required `plugin:` key is missing. Add it, even if the value is an empty list. |
| `Invalid type for plugin: <id> key: <key>` | A key has the wrong type, for example `author` as a string instead of a list. |
| `Invalid flags for plugin: <id>` | `flags` contains an unknown value. |
| `Skipping plugin "<folder>": conf.yml identifier "<x>" does not match folder name` | Rename the folder or fix `plugin.identifier`. |
| `Plugin <id> has unmet dependencies!` | A `php=`, `php-ext=`, `composer=` or `plugin=` entry is not satisfied. The admin plugin page lists which. |
| `Skipping plugin "<id>": entry class could not be resolved. Ensure plugin.name, the PHP class name, and namespace App\Addons\<id> all match.` | The loader looks for `App\Addons\<id>\<plugin.name>`. Check the namespace line, the class name, the file name and that the class implements `App\Plugins\AppPlugin`. |
| `Failed to process plugin event` | Your `processEvents()` threw. The message is not logged, so wrap its body in `try`/`catch` and log the exception yourself while you debug. |
| `Failed to load plugin routes from <file>: <message>` | A route file threw while loading. Only that file is skipped. |
| `Skipping addon route file whose name matches another addon identifier` | A file in your `Routes/` has the same name as another installed plugin. Rename it. |
| `Missing required config '<key>' for plugin: <id>` | A key from `requiredConfigs` has no stored value. The plugin still loads. |

> [!TIP]
> Route files and cron classes are loaded for every folder in `storage/addons/`, even when the plugin itself was
> skipped. A plugin that is "not loaded" can still answer its routes, which can hide a broken manifest.

## Errors when installing a package

Uploading or installing an `.fpa` checks the entry class before anything is copied. The API answers `422` with:

| Error code | Meaning |
| --- | --- |
| `ADDON_INVALID` | No `conf.yml` at the root of the archive. The files must not sit inside a folder. |
| `ADDON_CONF_PARSE_FAILED` | `conf.yml` is not valid YAML. |
| `ADDON_IDENTIFIER_INVALID` | The identifier in `conf.yml` is missing or invalid. |
| `ADDON_ENTRY_INVALID` | The entry class check failed. `data.errors` lists every problem, for example `Entry class namespace "..." does not match required "App\Addons\<id>"`. |
| `ADDON_MIGRATION_FAILED` | A file in `Migrations/` failed. The response includes the migration output. |

## Common problems

### A route returns API_ROUTE_NOT_FOUND

- The file is not under `Routes/`, does not end in `.php`, or does not `return function (RouteCollection $routes)`.
- The route file threw while loading. Search the log for `Failed to load plugin routes`.
- The API runs in worker mode and the worker started before you added the file. Click **Resync** on the plugin page
  or restart the backend container. Any successful admin change under `/api/admin/plugins` makes the workers
  restart.

### PHP changes do not show up

In worker mode, which the Docker image uses by default, each worker keeps your classes in memory until it restarts.
Click **Resync**, save a plugin setting, or restart the backend container. To develop without restarts, set
`FRANKENPHP_WORKER=false` on the backend container so every request loads the code again. See
[Middleware and router hooks](https://featherpanel.com/docs/plugins/middleware-and-hooks/#worker-mode).

### Frontend/index.js or index.css changes do not show up

The panel joins every plugin's `index.js` (and `index.css`) and caches the result for two minutes, and the browser may
keep it for another minute. Wait, then hard reload. Each plugin's script is wrapped in its own function, so a variable
you expect on `window` must be assigned to `window` explicitly. If your script uses `window.FeatherPanel`, wait until
it exists, as the Starter template does.

### A sidebar page is empty or shows the wrong file

- `component` is a path inside `Frontend/Components/`. The panel loads `/components/<identifier>/<component>`.
- After copying files by hand, click **Resync** so `public/components/<identifier>` points at your folder.
- Open the component URL directly in the browser to see the real error.

### A cron job never runs

- The class must be `App\Addons\<identifier>\Cron\<file name>`. The cron log shows `Class ... not found` otherwise.
- The interval unit must be upper case: `5M`, not `5m`.
- An earlier PHP `Error` (not `Exception`) in any job stops the whole run. Check the cron log for a fatal error.
- Delete `storage/caches/cron/<id>.fptj` to force the next run.

### A setting is always empty

`default` in `conf.yml` is only a placeholder in the form. Seed values in `pluginInstall()` or fall back in code. See
[Settings](https://featherpanel.com/docs/plugins/settings/).

## Checklist before you release

- `conf.yml` has every required key, a bumped `version`, and an identifier that matches the folder.
- The validate endpoint reports `overall_valid: true`.
- The entry class is the only PHP file in the plugin root.
- Route names and paths are prefixed with your identifier.
- Every value from a request reaches SQL through prepared statements.
- Listeners and `processEvents()` catch their own exceptions.
- Nothing user-specific is kept in static properties.
- Cron classes use `App\Addons\<identifier>\Cron` and upper-case intervals.
- Migrations use `IF NOT EXISTS` and table names start with `featherpanel_<identifier>_`.
- `pluginUninstall()` removes the tables and settings the plugin created.
- The plugin installs from an `.fpa` on a clean panel. See [Packaging and publishing](https://featherpanel.com/docs/plugins/packaging-and-publishing/).

## Related

- [Plugin FAQ](https://featherpanel.com/docs/plugins/faq/)
- [Middleware and router hooks](https://featherpanel.com/docs/plugins/middleware-and-hooks/)
- [Cron jobs and CLI commands](https://featherpanel.com/docs/plugins/cron-and-commands/)

## Frequently asked questions

### Where are the panel logs?

The application log is backend/storage/logs/App.fplog, which is /var/www/html/storage/logs/App.fplog in the Docker backend container. Admins with admin.root can also read it on the Logs page in the admin area.

### My plugin shows as installed but not loaded. Why?

Usually the conf.yml is invalid, the identifier does not match the folder, a dependency is missing or the entry class name, file name and namespace do not match. The log names the exact reason; search it for your identifier.
