Vendors
Model Types
Models
{{ tab.tab }}
```
**Key behavior:**
- Routes registered via `router.addRoute()` persist for the session
- Components lazy-load when the tab is clicked (code-splitting preserved)
- Tabs only appear if the plugin is enabled and staged
## Constraints as implemented
What the sections above describe is the mechanism as designed. What ships is
narrower, and a plugin author reading only the hook signature would not find
this out until a tab failed to appear in front of a user. The three constraints
below are the ones the contract does not express.
**One hub draws plugin tabs: Model Catalog.** `router.addRoute` appears exactly
once in the frontend, in `frontend/src/views/settings/ModelCatalogHub.vue`.
`AssetConfigHub`, `SiteConfigHub`, `NetworkHub`, `SecurityHub`, `LocationsHub`,
`IntegrationsHub` and `AssetTypeHub` render their own tabs and nothing else, so
a route declaring any of their ids is accepted by the hook, returned by the API
and drawn by nobody. Adding a hub to the supported set means writing the
fetch-and-register code into that hub's `.vue` file, not editing a list. The
supported set is named once, in `SETTINGS_HUBS_WITH_PLUGIN_TABS`
(`shopdb/plugins/base.py`); `/api/plugins/settings-routes` drops a route outside
it and logs an error naming the plugin and the hub, the plugin manager logs the
same finding at startup, and `tests/test_core/test_plugin_settings_hubs.py`
fails if a bundled plugin declares an unsupported hub or if the list and the
frontend drift apart.
**A plugin tab is not deep-linkable.** The hub registers its plugin routes in
`onMounted`. Before the hub has mounted, the router has no such route, so
navigating straight to a plugin tab URL - from a link, a bookmark, or a refresh
while sitting on one - resolves against a router that does not know the path.
Only arriving through the parent hub works. This is a real limit on what a
plugin settings page can be: it cannot be somewhere you send a person.
**The component does not load in a production build at all.** The `component`
field is a server-supplied string interpolated into a dynamic import,
``() => import(`../../${tab.component}`)``. Vite refuses that template - it has
no static file extension - and says so during every build:
[plugin vite:dynamic-import-vars] src/views/settings/ModelCatalogHub.vue:
invalid import "../../${tab.component}". A file extension must be included
in the static part of the import.
The build still succeeds, and the emitted chunk carries the expression verbatim
(`import(`../../${e.component}`)`), rewritten to nothing. At runtime the browser
resolves it against the chunk's own URL under `/assets/` and asks the server for
a `.vue` file; `dist/` contains no `.vue` files, so the request 404s and the tab
throws when it is clicked. Under `npm run dev` the same import works, because
the dev server serves sources by path. So the mechanism as it stands is a
development-only path: a tab that appears in production and fails on the click.
Nothing visibly broke because the one plugin using it does not depend on it.
`plugins/computers/frontend/routes.js` also registers the same page statically
at `/settings/operatingsystems`, and that route - a normal static import, a real
URL - is what production actually uses. The dynamic tab is a duplicate way in.
Two ways out, and this ADR does not pick one: give the template a static
extension (declare `component` without `.vue` and import ``${component}.vue``),
which makes Vite glob every `.vue` under `frontend/src` at build time and makes
the tab real for staged plugins while staying build-time bound; or accept that
plugin settings pages are ordinary plugin routes and drop the hub-tab mechanism.
Deciding that is the open question the architectural review raised.
**What a plugin author must therefore do:**
1. Declare `hub: 'model-catalog'` or declare nothing. For a settings page that
belongs anywhere else, own a full page under the plugin's own routes
(`plugins/