diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d0b1566..2e0437ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,27 @@ ADR-007 and ADR-002. ### Fixed +- **A search result opened the list it sits in, not the record it found.** A + NOTICE linked to `/notifications`, so clicking the row search had already + identified sent the reader to the list page to find it again by hand. The id + was in the payload the whole time and only the URL ignored it. The subnet + searcher had the same defect: an IP search resolves which subnet an address + falls inside, then linked to the `/network` hub rather than that subnet. + Both now link to the record. Every other result type already carried its id. + +- **`flask plugin new` scaffolded a frontend that no lean build could prune.** + It wrote views to `frontend/src/views//` and a route file to + `frontend/src/router/routes/.js`, while all 16 bundled plugins keep + their frontend in `plugins//frontend/`, staged by + `stage-frontend.mjs`. Nothing looked broken, which is why it lasted: the + router still globs `./routes/*.js` during the Phase 4 dual-location + transition, so the pages loaded. What a new plugin author silently lost was + ADR-013 lean builds, since pruning works off the staged tree, plus stranded + views on deleting the plugin. The templates for the right layout already + existed; only the output paths were wrong. Their core imports moved to the + `@` alias, which resolves the same before and after staging, where the + previous `../../api` was correct only in the old location. + - **A zero bar height printed a barcode 176 wide and 0 high.** Nothing rejected it: the value went straight into a CSS length, `height: 0in` is perfectly valid, and an image at `height: 100%` of a zero-height box is zero pixels diff --git a/shopdb/core/api/search.py b/shopdb/core/api/search.py index a308bc5b..d023304c 100644 --- a/shopdb/core/api/search.py +++ b/shopdb/core/api/search.py @@ -658,7 +658,10 @@ def _search_subnets(query): 'id': subnet.subnetid, 'title': f'{subnet.name} ({subnet.cidr})', 'subtitle': subnet.description or subnet.subnettype, - 'url': f'/network', + # the matched subnet, not the hub: searching an IP + # already identified WHICH subnet, so landing on the + # hub makes the reader find it again by hand + 'url': f'/networks/{subnet.subnetid}', 'relevance': 70 }) except ValueError: @@ -779,7 +782,9 @@ def _search_notifications(query, search_term): 'id': notif.notificationid, 'title': notif.title, 'subtitle': notif.notificationtype.typename if notif.notificationtype else None, - 'url': f'/notifications', + # the notification itself, not the list it sits in: the + # `notification-detail` route renders one by id + 'url': f'/notifications/{notif.notificationid}', 'relevance': min(int(base_relevance), 100), 'ticketnumber': notif.ticketnumber, 'iscurrent': notif.is_current diff --git a/shopdb/plugins/scaffolder.py b/shopdb/plugins/scaffolder.py index ea6eebe7..acb70434 100644 --- a/shopdb/plugins/scaffolder.py +++ b/shopdb/plugins/scaffolder.py @@ -81,18 +81,28 @@ def _scaffold_frontend( name: str, substitutions: dict, plugin_target: Path, - frontend_dir: Path, template_root: Path, overwrite: bool, ) -> None: """Render the frontend starting points for a scaffolded plugin. - Writes the paste-in api-client snippet into the plugin directory, then the - Vue views and the router route file into the real frontend tree. The - snippet is emitted regardless of whether the frontend tree exists, because - it is a plugin-directory artifact useful even for external-repo plugins. - Views and the route file are skipped when frontend_dir is missing, which is - the normal case for a plugin developed in its own repository. + Everything lands INSIDE the plugin, under `plugins//frontend/`: + `routes.js` plus `views/{List,Detail,Form}.vue`, and the paste-in + api-client snippet at the plugin root. + + The plugin owns its frontend because that is what makes it prunable. + scripts/stage-frontend.mjs copies each plugin's `frontend/` into + `frontend/src/.plugins-staged//` and aggregates the route modules + into `routes.gen.js`; a plugin left out of SITE_PLUGINS is simply not + copied, so its frontend leaves the build with it (ADR-013). A route file + written into the CORE tree instead is still picked up - the router globs + `./routes/*.js` during the Phase 4 dual-location transition - but it is + invisible to that pruning, and deleting the plugin strands its views. + + This is also why nothing here depends on the core frontend tree existing. + A plugin developed in its own repo gets exactly the same layout, and its + frontend starts working the moment the plugin directory is dropped into + `plugins/`. Ordering matters: every view is written before the route file. A route file that lazy-imports a view that is not on disk crashes the Vite dev server, @@ -104,7 +114,7 @@ def _scaffold_frontend( plugin_name = substitutions['Name'] - # snippet lands in the plugin dir; author pastes it into api/index.js + # snippet lands at the plugin root; author pastes it into api/index.js snippet_template = fe_templates / 'frontend-api-snippet.js.tmpl' if snippet_template.exists(): _render_template( @@ -114,11 +124,8 @@ def _scaffold_frontend( overwrite, ) - # views and route need the real frontend tree; external repos skip these - if not frontend_dir.exists(): - return - - views_dir = frontend_dir / 'views' / name + frontend_dir = plugin_target / 'frontend' + views_dir = frontend_dir / 'views' # views first: route file lazy-imports them, missing views 500 vite view_templates = { @@ -141,7 +148,7 @@ def _scaffold_frontend( if route_template.exists(): _render_template( route_template, - frontend_dir / 'router' / 'routes' / f'{name}.js', + frontend_dir / 'routes.js', substitutions, overwrite, ) @@ -154,7 +161,6 @@ def scaffold_plugin( template_root: Optional[Path] = None, overwrite: bool = False, frontend: bool = True, - frontend_dir: Optional[Path] = None, ) -> Path: """Generate a new plugin from templates. @@ -166,11 +172,10 @@ def scaffold_plugin( overwrite: If True, overwrite an existing plugin directory and any existing generated frontend files frontend: If True, also render the Vue frontend starting points (list, - detail, form views, a route file, and a paste-in api-client snippet) - frontend_dir: Frontend src directory to render views/routes into - (default: /../frontend/src). Views and the route file - are skipped when this directory does not exist, which is the normal - case for a plugin developed in its own repository. + detail, form views, a route file, and a paste-in api-client + snippet) into `plugins//frontend/`. There is no separate + output directory: the plugin owns its frontend so that + stage-frontend.mjs can stage it and a lean build can prune it. Returns: Path to the generated plugin directory. @@ -205,7 +210,9 @@ def scaffold_plugin( for template_path in template_root.rglob('*.tmpl'): rel = template_path.relative_to(template_root) - # frontend templates render into the frontend tree, not the plugin dir + # frontend templates are handled by _scaffold_frontend: they are + # renamed on the way out (List.vue.tmpl -> List.vue) and are + # skipped entirely when frontend=False if rel.parts and rel.parts[0] == 'frontend': continue @@ -222,13 +229,10 @@ def scaffold_plugin( out_path.write_text(rendered) if frontend: - if frontend_dir is None: - frontend_dir = plugins_dir.parent / 'frontend' / 'src' _scaffold_frontend( name=name, substitutions=substitutions, plugin_target=target, - frontend_dir=Path(frontend_dir), template_root=template_root, overwrite=overwrite, ) diff --git a/shopdb/plugins/templates/frontend/routes.js.tmpl b/shopdb/plugins/templates/frontend/routes.js.tmpl index 758b6f70..1616ee33 100644 --- a/shopdb/plugins/templates/frontend/routes.js.tmpl +++ b/shopdb/plugins/templates/frontend/routes.js.tmpl @@ -1,35 +1,49 @@ /** * ${Name} plugin routes. * - * Auto-discovered by the router via import.meta.glob, so no registration - * edit is needed. Every route carries meta.plugin '${name}' so the ADR-009 - * guard redirects to the dashboard when the ${name} backend plugin is - * disabled. Form routes add requiresAuth so anonymous users cannot reach - * create or edit. + * This file lives in the plugin, not in the core frontend tree. + * scripts/stage-frontend.mjs copies plugins/${name}/frontend/ into + * frontend/src/.plugins-staged/${name}/ and aggregates this default export + * into routes.gen.js, which the router spreads in - so no registration edit + * is needed, and pruning the plugin for a lean build removes its frontend + * with it (ADR-013). + * + * Every route carries meta.plugin '${name}' so the ADR-009 guard redirects to + * the dashboard when the ${name} backend plugin is disabled. Form routes add + * requiresAuth so anonymous users cannot reach create or edit. + * + * View imports are relative (./views/...) because they travel with this file. + * Anything from the CORE tree uses the @ alias (@/api, @/components/...), + * which resolves the same before and after staging. */ export default [ { path: '${name}', name: '${name}', - component: () => import('../../views/${name}/${Name}List.vue'), + component: () => import('./views/${Name}List.vue'), meta: { plugin: '${name}' } }, { path: '${name}/new', name: '${name}-new', - component: () => import('../../views/${name}/${Name}Form.vue'), + component: () => import('./views/${Name}Form.vue'), meta: { requiresAuth: true, plugin: '${name}' } }, { path: '${name}/:id', name: '${name}-detail', - component: () => import('../../views/${name}/${Name}Detail.vue'), + component: () => import('./views/${Name}Detail.vue'), meta: { plugin: '${name}' } }, { path: '${name}/:id/edit', name: '${name}-edit', - component: () => import('../../views/${name}/${Name}Form.vue'), + component: () => import('./views/${Name}Form.vue'), meta: { requiresAuth: true, plugin: '${name}' } } ] + +// Full-screen routes that render OUTSIDE AppLayout (kiosk, print pages). +// Declared even when empty so routes.gen.js can read `.toplevel` off the +// module without Rollup warning about a missing export. +export const toplevel = [] diff --git a/shopdb/plugins/templates/frontend/views/Detail.vue.tmpl b/shopdb/plugins/templates/frontend/views/Detail.vue.tmpl index 9777a799..b1dc5ec1 100644 --- a/shopdb/plugins/templates/frontend/views/Detail.vue.tmpl +++ b/shopdb/plugins/templates/frontend/views/Detail.vue.tmpl @@ -78,12 +78,12 @@