webspresso 0.0.81 → 0.0.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -15
- package/bin/commands/skill.js +6 -4
- package/package.json +1 -1
- package/plugins/admin-panel/app.js +51 -1
- package/plugins/admin-panel/core/admin-module.js +170 -17
- package/plugins/admin-panel/core/registry.js +3 -0
- package/plugins/admin-panel/index.js +17 -1
- package/plugins/audit-log/admin-component.js +2 -0
- package/templates/skills/webspresso-usage/SKILL.md +2 -1
package/README.md
CHANGED
|
@@ -245,9 +245,10 @@ webspresso doctor --strict
|
|
|
245
245
|
|
|
246
246
|
### `webspresso skill`
|
|
247
247
|
|
|
248
|
-
Scaffold
|
|
248
|
+
Scaffold an **Agent Skill**: creates `.agents/skills/<name>/SKILL.md` with valid YAML frontmatter (`name`, `description`) and a short markdown template for AI tooling. Use `--global` to write under `~/.agents/skills/` instead of the current project.
|
|
249
|
+
|
|
250
|
+
**Bundled preset:** `--preset webspresso` copies **`SKILL.md`** (short index), **`REFERENCE-framework.md`** (SSR `createApp`, routes, ORM, auth, plugins, CLI), and **`REFERENCE-kernel.md`** (`kernel` event bus / flows) into **`.agents/skills/webspresso-usage/`** — no prompts.
|
|
249
251
|
|
|
250
|
-
**Bundled preset:** `--preset webspresso` copies **`SKILL.md`** (short index), **`REFERENCE-framework.md`** (SSR `createApp`, routes, ORM, auth, plugins, CLI), and **`REFERENCE-kernel.md`** (`kernel` event bus / flows) into **`.cursor/skills/webspresso-usage/`** — no prompts.
|
|
251
252
|
|
|
252
253
|
```bash
|
|
253
254
|
webspresso skill my-workflow
|
|
@@ -805,16 +806,18 @@ Options:
|
|
|
805
806
|
- `userManagement` - Site-user admin UI (`enabled`, `model` matching ORM user table, optional `fields` map). SPA routes: `/_admin/users`, `/_admin/users/new`, …; APIs: `/_admin/api/users*`. Admin staff still use **`admin_users`** / `/_admin` login; this is separate from **`req.user`** on the public site
|
|
806
807
|
- `configure` - Callback `(registry) => void` for manual setup
|
|
807
808
|
|
|
808
|
-
See **`doc/index.html#admin-user-management`** and **Session authentication** in **`.
|
|
809
|
+
See **`doc/index.html#admin-user-management`** and **Session authentication** in **`.agents/skills/webspresso-usage/REFERENCE-framework.md`** for the split between **`adminUser`** and **`createApp({ auth })`**.
|
|
810
|
+
|
|
809
811
|
|
|
810
812
|
**Custom Admin Pages (registerModule):**
|
|
811
813
|
|
|
812
|
-
Plugins can add custom admin pages using `registerModule` in `onRoutesReady`:
|
|
814
|
+
Plugins can add custom admin pages using `registerModule` (or `registerPageDir`) in `onRoutesReady`:
|
|
813
815
|
|
|
814
816
|
```javascript
|
|
815
817
|
// In your plugin's onRoutesReady(ctx)
|
|
816
818
|
const adminApi = ctx.usePlugin('admin-panel');
|
|
817
819
|
if (adminApi) {
|
|
820
|
+
// Option 1: File-based registration with auto Layout wrapping
|
|
818
821
|
adminApi.registerModule({
|
|
819
822
|
id: 'my-module',
|
|
820
823
|
|
|
@@ -824,7 +827,8 @@ if (adminApi) {
|
|
|
824
827
|
path: '/reports',
|
|
825
828
|
icon: 'chart',
|
|
826
829
|
description: 'View reports',
|
|
827
|
-
|
|
830
|
+
componentFile: path.join(__dirname, 'admin/reports.js'), // JS component file
|
|
831
|
+
layout: true, // Auto-wraps view in Admin Layout (default: true)
|
|
828
832
|
}],
|
|
829
833
|
|
|
830
834
|
menu: [{ id: 'reports', label: 'Reports', path: '/reports', icon: 'chart', order: 5 }],
|
|
@@ -835,15 +839,10 @@ if (adminApi) {
|
|
|
835
839
|
{ method: 'get', path: '/summary', handler: getSummaryHandler, auth: true },
|
|
836
840
|
],
|
|
837
841
|
},
|
|
838
|
-
|
|
839
|
-
widgets: [{
|
|
840
|
-
id: 'reports-widget',
|
|
841
|
-
title: 'Quick Stats',
|
|
842
|
-
dataLoader: async () => ({ count: 42 }),
|
|
843
|
-
}],
|
|
844
|
-
|
|
845
|
-
menuGroups: [{ id: 'analytics', label: 'Analytics', order: 2 }],
|
|
846
842
|
});
|
|
843
|
+
|
|
844
|
+
// Option 2: Folder-based auto registration (dir containing page.json + component.js)
|
|
845
|
+
adminApi.registerPageDir(path.join(__dirname, 'admin/pages'));
|
|
847
846
|
}
|
|
848
847
|
```
|
|
849
848
|
|
|
@@ -851,13 +850,41 @@ if (adminApi) {
|
|
|
851
850
|
| Field | Description |
|
|
852
851
|
|-------|-------------|
|
|
853
852
|
| `id` | Unique module identifier (required) |
|
|
854
|
-
| `pages` | Custom admin pages (
|
|
853
|
+
| `pages` | Custom admin pages (`id`, `title`, `path`, `icon`, `description`, `componentFile`, `component`, `layout`) |
|
|
854
|
+
| `pagesDir` | Path to directory containing page folders/configs for automatic discovery |
|
|
855
855
|
| `menu` | Sidebar menu items (`id`, `label`, `path`, `icon`, `order`) |
|
|
856
856
|
| `menuGroups` | Collapsible menu groups (`id`, `label`, `order`) |
|
|
857
857
|
| `api` | API routes (`prefix`, `routes`: `method`, `path`, `handler`, `auth`) |
|
|
858
858
|
| `widgets` | Dashboard widgets (`id`, `title`, `dataLoader`) |
|
|
859
859
|
|
|
860
|
-
|
|
860
|
+
Pages support `componentFile` (path to a `.js` file exporting/returning a Mithril component), `pagesDir` (directory auto-discovery), or `component` (code string). By default (`layout: true`), page views are automatically wrapped in the Admin Layout with Breadcrumbs. Set `layout: false` for full-screen / custom canvas pages.
|
|
861
|
+
|
|
862
|
+
**Example component file (`admin/reports.js`):**
|
|
863
|
+
|
|
864
|
+
```javascript
|
|
865
|
+
// Clean Mithril.js component object (no manual m(Layout, ...) needed!)
|
|
866
|
+
({
|
|
867
|
+
oninit: async (vnode) => {
|
|
868
|
+
vnode.state.data = [];
|
|
869
|
+
try {
|
|
870
|
+
const res = await api.get('/reports/summary');
|
|
871
|
+
vnode.state.data = res.data || [];
|
|
872
|
+
m.redraw();
|
|
873
|
+
} catch (err) {
|
|
874
|
+
console.error('Failed to load report data:', err);
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
view: (vnode) => {
|
|
878
|
+
const data = vnode.state.data || [];
|
|
879
|
+
return m('div.space-y-4', [
|
|
880
|
+
m('h1.text-2xl.font-bold.text-gray-900.dark:text-slate-100', 'Report Summary'),
|
|
881
|
+
m('p.text-gray-600.dark:text-slate-400', `Total records: ${data.length}`),
|
|
882
|
+
]);
|
|
883
|
+
}
|
|
884
|
+
})
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
|
|
861
888
|
|
|
862
889
|
**Manual registry API** (alternative to registerModule):
|
|
863
890
|
|
package/bin/commands/skill.js
CHANGED
|
@@ -20,9 +20,10 @@ function validateSkillName(name) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function skillDir(base, skillName) {
|
|
23
|
-
return path.join(base, '.
|
|
23
|
+
return path.join(base, '.agents', 'skills', skillName);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
|
|
26
27
|
function defaultDescription(skillName) {
|
|
27
28
|
const label = skillName.replace(/-/g, ' ');
|
|
28
29
|
return `Guides the agent through tasks for ${label}. Use when the user works on ${label} or asks about related workflows.`;
|
|
@@ -72,14 +73,15 @@ Example prompt or command
|
|
|
72
73
|
function registerCommand(program) {
|
|
73
74
|
program
|
|
74
75
|
.command('skill [name]')
|
|
75
|
-
.description('Create an Agent Skill folder with SKILL.md
|
|
76
|
-
.option('-g, --global', 'Write to ~/.
|
|
76
|
+
.description('Create an Agent Skill folder with SKILL.md')
|
|
77
|
+
.option('-g, --global', 'Write to ~/.agents/skills/ instead of ./.agents/skills/')
|
|
77
78
|
.option('-d, --description <text>', 'Skill description (for YAML frontmatter)')
|
|
78
79
|
.option('-f, --force', 'Overwrite existing SKILL.md')
|
|
79
80
|
.option(
|
|
80
81
|
'-p, --preset <name>',
|
|
81
|
-
'Install bundled skill: webspresso → agent reference (SKILL.md + REFERENCE-*.md in .
|
|
82
|
+
'Install bundled skill: webspresso → agent reference (SKILL.md + REFERENCE-*.md in .agents/skills/webspresso-usage/)',
|
|
82
83
|
)
|
|
84
|
+
|
|
83
85
|
.action(async (nameArg, options) => {
|
|
84
86
|
const presetKey = options.preset ? String(options.preset).trim().toLowerCase() : '';
|
|
85
87
|
|
package/package.json
CHANGED
|
@@ -221,6 +221,55 @@ var routes = {
|
|
|
221
221
|
},
|
|
222
222
|
};
|
|
223
223
|
|
|
224
|
+
function wrapCustomPageWithLayout(page, rawComp) {
|
|
225
|
+
if (!rawComp) return createCustomPage(page);
|
|
226
|
+
if (page.layout === false) return rawComp;
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
oninit: function(vnode) {
|
|
230
|
+
vnode.state.instance = typeof rawComp === 'function' ? rawComp.call(this, vnode) : rawComp;
|
|
231
|
+
if (vnode.state.instance && typeof vnode.state.instance.oninit === 'function') {
|
|
232
|
+
return vnode.state.instance.oninit.call(this, vnode);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
oncreate: function(vnode) {
|
|
236
|
+
const comp = vnode.state.instance || rawComp;
|
|
237
|
+
if (comp && typeof comp.oncreate === 'function') return comp.oncreate.call(this, vnode);
|
|
238
|
+
},
|
|
239
|
+
onupdate: function(vnode) {
|
|
240
|
+
const comp = vnode.state.instance || rawComp;
|
|
241
|
+
if (comp && typeof comp.onupdate === 'function') return comp.onupdate.call(this, vnode);
|
|
242
|
+
},
|
|
243
|
+
onbeforeremove: function(vnode) {
|
|
244
|
+
const comp = vnode.state.instance || rawComp;
|
|
245
|
+
if (comp && typeof comp.onbeforeremove === 'function') return comp.onbeforeremove.call(this, vnode);
|
|
246
|
+
},
|
|
247
|
+
onremove: function(vnode) {
|
|
248
|
+
const comp = vnode.state.instance || rawComp;
|
|
249
|
+
if (comp && typeof comp.onremove === 'function') return comp.onremove.call(this, vnode);
|
|
250
|
+
},
|
|
251
|
+
view: function(vnode) {
|
|
252
|
+
const comp = vnode.state.instance || rawComp;
|
|
253
|
+
var content;
|
|
254
|
+
if (comp && typeof comp.view === 'function') {
|
|
255
|
+
content = comp.view.call(this, vnode);
|
|
256
|
+
} else if (typeof comp === 'function') {
|
|
257
|
+
content = comp.call(this, vnode);
|
|
258
|
+
} else {
|
|
259
|
+
content = comp;
|
|
260
|
+
}
|
|
261
|
+
if (content && (content.tag === Layout || (content.tag && content.tag.name === 'Layout'))) {
|
|
262
|
+
return content;
|
|
263
|
+
}
|
|
264
|
+
var breadcrumbs = [{ label: page.title, href: page.path }];
|
|
265
|
+
return m(Layout, [
|
|
266
|
+
m(Breadcrumb, { items: breadcrumbs }),
|
|
267
|
+
content
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
224
273
|
// Dynamic routes for custom pages registered via plugins
|
|
225
274
|
var config = window.__ADMIN_CONFIG__;
|
|
226
275
|
if (config && config.pages) {
|
|
@@ -234,7 +283,7 @@ if (config && config.pages) {
|
|
|
234
283
|
return;
|
|
235
284
|
}
|
|
236
285
|
if (window.__customPages && window.__customPages[page.id]) {
|
|
237
|
-
return window.__customPages[page.id];
|
|
286
|
+
return wrapCustomPageWithLayout(page, window.__customPages[page.id]);
|
|
238
287
|
}
|
|
239
288
|
return createCustomPage(page);
|
|
240
289
|
}
|
|
@@ -245,3 +294,4 @@ if (config && config.pages) {
|
|
|
245
294
|
|
|
246
295
|
m.route(document.getElementById('app'), '/', routes);
|
|
247
296
|
`;
|
|
297
|
+
|
|
@@ -1,25 +1,118 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Declarative API for registering admin pages, menu items, API routes, and widgets
|
|
4
|
-
* @module plugins/admin-panel/core/admin-module
|
|
5
|
+
* Format client component code to ensure it assigns to window.__customPages[pageId]
|
|
5
6
|
*/
|
|
7
|
+
function formatClientComponent(pageId, rawCode) {
|
|
8
|
+
let code = (rawCode || '').trim();
|
|
9
|
+
if (!code) return '';
|
|
10
|
+
if (code.includes('window.__customPages')) {
|
|
11
|
+
return code;
|
|
12
|
+
}
|
|
13
|
+
if (code.startsWith('module.exports =')) {
|
|
14
|
+
code = code.replace(/^module\.exports\s*=\s*/, '');
|
|
15
|
+
}
|
|
16
|
+
if (code.endsWith(';')) {
|
|
17
|
+
code = code.slice(0, -1);
|
|
18
|
+
}
|
|
19
|
+
return `window.__customPages = window.__customPages || {};\nwindow.__customPages[${JSON.stringify(pageId)}] = ${code};`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load page definitions and components from a directory structure
|
|
24
|
+
*/
|
|
25
|
+
function loadPagesFromDir(dirPath) {
|
|
26
|
+
const resolvedDir = path.resolve(dirPath);
|
|
27
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
28
|
+
throw new Error(`pagesDir directory not found: ${dirPath}`);
|
|
29
|
+
}
|
|
30
|
+
const stat = fs.statSync(resolvedDir);
|
|
31
|
+
if (!stat.isDirectory()) {
|
|
32
|
+
throw new Error(`pagesDir path is not a directory: ${dirPath}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
|
|
36
|
+
const pages = [];
|
|
37
|
+
const menuItems = [];
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (entry.name.startsWith('.')) continue;
|
|
41
|
+
|
|
42
|
+
let pageConfig = null;
|
|
43
|
+
let componentFile = null;
|
|
44
|
+
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
const folderPath = path.join(resolvedDir, entry.name);
|
|
47
|
+
const jsonCandidate = path.join(folderPath, 'page.json');
|
|
48
|
+
const configJsonCandidate = path.join(folderPath, 'config.json');
|
|
49
|
+
|
|
50
|
+
if (fs.existsSync(jsonCandidate)) {
|
|
51
|
+
pageConfig = JSON.parse(fs.readFileSync(jsonCandidate, 'utf8'));
|
|
52
|
+
} else if (fs.existsSync(configJsonCandidate)) {
|
|
53
|
+
pageConfig = JSON.parse(fs.readFileSync(configJsonCandidate, 'utf8'));
|
|
54
|
+
} else {
|
|
55
|
+
pageConfig = { id: entry.name, title: entry.name, path: '/' + entry.name };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const jsCandidates = ['component.js', 'index.js', 'page.js', `${entry.name}.js`];
|
|
59
|
+
for (const cand of jsCandidates) {
|
|
60
|
+
const fullCandPath = path.join(folderPath, cand);
|
|
61
|
+
if (fs.existsSync(fullCandPath)) {
|
|
62
|
+
componentFile = fullCandPath;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
67
|
+
const jsonPath = path.join(resolvedDir, entry.name);
|
|
68
|
+
pageConfig = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
69
|
+
const baseName = entry.name.slice(0, -5);
|
|
70
|
+
const jsCandidate = path.join(resolvedDir, baseName + '.js');
|
|
71
|
+
if (fs.existsSync(jsCandidate)) {
|
|
72
|
+
componentFile = jsCandidate;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (pageConfig) {
|
|
77
|
+
if (componentFile && !pageConfig.componentFile) {
|
|
78
|
+
pageConfig.componentFile = componentFile;
|
|
79
|
+
}
|
|
80
|
+
if (pageConfig.menu) {
|
|
81
|
+
if (pageConfig.menu === true) {
|
|
82
|
+
menuItems.push({
|
|
83
|
+
id: pageConfig.id,
|
|
84
|
+
label: pageConfig.title,
|
|
85
|
+
path: pageConfig.path,
|
|
86
|
+
icon: pageConfig.icon,
|
|
87
|
+
});
|
|
88
|
+
} else if (typeof pageConfig.menu === 'object') {
|
|
89
|
+
menuItems.push({
|
|
90
|
+
id: pageConfig.id,
|
|
91
|
+
label: pageConfig.title,
|
|
92
|
+
path: pageConfig.path,
|
|
93
|
+
icon: pageConfig.icon,
|
|
94
|
+
...pageConfig.menu,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
pages.push(pageConfig);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { pages, menuItems };
|
|
103
|
+
}
|
|
6
104
|
|
|
7
105
|
/**
|
|
8
106
|
* Register an admin module with all its components in a single declarative call
|
|
9
107
|
* @param {Object} config - Module configuration
|
|
10
108
|
* @param {string} config.id - Unique module identifier (required)
|
|
11
109
|
* @param {Array} [config.pages] - Custom admin pages
|
|
110
|
+
* @param {string} [config.pagesDir] - Directory path containing page folders/configs
|
|
12
111
|
* @param {Array} [config.menu] - Sidebar menu items
|
|
13
112
|
* @param {Object} [config.menuGroups] - Menu group definitions
|
|
14
113
|
* @param {Object} [config.api] - API endpoint definitions
|
|
15
114
|
* @param {Array} [config.widgets] - Dashboard widgets
|
|
16
115
|
* @param {Object} deps - Internal dependencies injected by admin-panel
|
|
17
|
-
* @param {Object} deps.registry - AdminRegistry instance
|
|
18
|
-
* @param {string} deps.adminPath - Admin panel base path
|
|
19
|
-
* @param {Object} deps.ctx - Plugin context
|
|
20
|
-
* @param {Function} deps.requireAuth - Auth middleware
|
|
21
|
-
* @param {Function} deps.optionalAuth - Optional auth middleware
|
|
22
|
-
* @param {Function} deps.serveAdminPanel - SPA HTML handler
|
|
23
116
|
*/
|
|
24
117
|
function registerModule(config, deps) {
|
|
25
118
|
if (!config || typeof config !== 'object') {
|
|
@@ -36,14 +129,36 @@ function registerModule(config, deps) {
|
|
|
36
129
|
registerMenuGroups(config.menuGroups, registry);
|
|
37
130
|
}
|
|
38
131
|
|
|
39
|
-
if (config.pages) {
|
|
40
|
-
|
|
132
|
+
if (config.pages !== undefined) {
|
|
133
|
+
if (!Array.isArray(config.pages)) {
|
|
134
|
+
throw new Error(`Module "${config.id}": pages must be an array`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (config.menu !== undefined) {
|
|
139
|
+
if (!Array.isArray(config.menu)) {
|
|
140
|
+
throw new Error('menu must be an array');
|
|
141
|
+
}
|
|
41
142
|
}
|
|
42
143
|
|
|
43
|
-
|
|
44
|
-
|
|
144
|
+
let pagesList = config.pages ? [...config.pages] : [];
|
|
145
|
+
let menuList = config.menu ? [...config.menu] : [];
|
|
146
|
+
|
|
147
|
+
if (config.pagesDir) {
|
|
148
|
+
const { pages: dirPages, menuItems: dirMenuItems } = loadPagesFromDir(config.pagesDir);
|
|
149
|
+
pagesList = pagesList.concat(dirPages);
|
|
150
|
+
menuList = menuList.concat(dirMenuItems);
|
|
45
151
|
}
|
|
46
152
|
|
|
153
|
+
if (pagesList.length > 0) {
|
|
154
|
+
registerPages(config.id, pagesList, { registry, adminPath, ctx, optionalAuth, serveAdminPanel });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (menuList.length > 0) {
|
|
158
|
+
registerMenuItems(menuList, registry);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
47
162
|
if (config.api) {
|
|
48
163
|
registerApiRoutes(config.id, config.api, { adminPath, ctx, requireAuth, optionalAuth });
|
|
49
164
|
}
|
|
@@ -53,6 +168,20 @@ function registerModule(config, deps) {
|
|
|
53
168
|
}
|
|
54
169
|
}
|
|
55
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Register custom admin pages from a directory directly
|
|
173
|
+
* @param {string} dirPath - Directory path containing custom page folders/configs
|
|
174
|
+
* @param {Object} deps - Dependencies injected by admin-panel
|
|
175
|
+
*/
|
|
176
|
+
function registerPageDir(dirPath, deps) {
|
|
177
|
+
const { pages, menuItems } = loadPagesFromDir(dirPath);
|
|
178
|
+
const moduleId = path.basename(dirPath);
|
|
179
|
+
registerPages(moduleId, pages, deps);
|
|
180
|
+
if (menuItems.length > 0) {
|
|
181
|
+
registerMenuItems(menuItems, deps.registry);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
56
185
|
function registerPages(moduleId, pages, deps) {
|
|
57
186
|
if (!Array.isArray(pages)) {
|
|
58
187
|
throw new Error(`Module "${moduleId}": pages must be an array`);
|
|
@@ -73,10 +202,20 @@ function registerPages(moduleId, pages, deps) {
|
|
|
73
202
|
icon: page.icon,
|
|
74
203
|
description: page.description,
|
|
75
204
|
permission: page.permission,
|
|
205
|
+
layout: page.layout !== undefined ? page.layout : true,
|
|
76
206
|
});
|
|
77
207
|
|
|
78
|
-
if (page.
|
|
79
|
-
|
|
208
|
+
if (page.componentFile) {
|
|
209
|
+
const resolvedPath = path.resolve(page.componentFile);
|
|
210
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
211
|
+
throw new Error(`Module "${moduleId}", page "${pageId}": componentFile not found at ${page.componentFile}`);
|
|
212
|
+
}
|
|
213
|
+
const rawCode = fs.readFileSync(resolvedPath, 'utf8');
|
|
214
|
+
const formattedCode = formatClientComponent(pageId, rawCode);
|
|
215
|
+
registry.registerClientComponent(pageId, formattedCode);
|
|
216
|
+
} else if (page.component) {
|
|
217
|
+
const formattedCode = formatClientComponent(pageId, page.component);
|
|
218
|
+
registry.registerClientComponent(pageId, formattedCode);
|
|
80
219
|
}
|
|
81
220
|
|
|
82
221
|
ctx.addRoute('get', `${adminPath}${page.path}`, optionalAuth, serveAdminPanel);
|
|
@@ -126,7 +265,20 @@ function registerApiRoutes(moduleId, apiConfig, deps) {
|
|
|
126
265
|
const useAuth = route.auth !== undefined ? route.auth : defaultAuth;
|
|
127
266
|
const authMiddleware = useAuth ? requireAuth : optionalAuth;
|
|
128
267
|
|
|
129
|
-
|
|
268
|
+
const safeHandler = async (req, res, next) => {
|
|
269
|
+
try {
|
|
270
|
+
await route.handler(req, res, next);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
if (typeof next === 'function') {
|
|
273
|
+
next(err);
|
|
274
|
+
} else {
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
safeHandler._originalHandler = route.handler;
|
|
280
|
+
|
|
281
|
+
ctx.addRoute(method, fullPath, authMiddleware, safeHandler);
|
|
130
282
|
}
|
|
131
283
|
}
|
|
132
284
|
|
|
@@ -143,4 +295,5 @@ function registerWidgets(widgets, registry) {
|
|
|
143
295
|
}
|
|
144
296
|
}
|
|
145
297
|
|
|
146
|
-
module.exports = { registerModule };
|
|
298
|
+
module.exports = { registerModule, registerPageDir, loadPagesFromDir, formatClientComponent };
|
|
299
|
+
|
|
@@ -412,9 +412,12 @@ class AdminRegistry {
|
|
|
412
412
|
title: p.title,
|
|
413
413
|
path: p.path,
|
|
414
414
|
icon: p.icon,
|
|
415
|
+
description: p.description,
|
|
415
416
|
permission: p.permission,
|
|
417
|
+
layout: p.layout !== undefined ? p.layout : true,
|
|
416
418
|
hasClientComponent: this.clientComponents.has(p.id),
|
|
417
419
|
})),
|
|
420
|
+
|
|
418
421
|
widgets: this.getWidgets().map(w => ({
|
|
419
422
|
id: w.id,
|
|
420
423
|
title: w.title,
|
|
@@ -15,7 +15,8 @@ const { registerDashboardWidgets, generateDashboardComponent } = require('./modu
|
|
|
15
15
|
const { registerDefaultBulkActions, generateBulkActionsComponent } = require('./modules/bulk-actions');
|
|
16
16
|
const { registerDefaultPages, createCustomPageApiHandlers, generateCustomPageComponent } = require('./modules/custom-pages');
|
|
17
17
|
const { registerModelMenuItems, registerSystemMenuItems, generateMenuComponent } = require('./modules/menu');
|
|
18
|
-
const { registerModule } = require('./core/admin-module');
|
|
18
|
+
const { registerModule, registerPageDir } = require('./core/admin-module');
|
|
19
|
+
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Admin Panel Plugin Factory
|
|
@@ -105,6 +106,21 @@ function adminPanelPlugin(options = {}) {
|
|
|
105
106
|
serveAdminPanel,
|
|
106
107
|
});
|
|
107
108
|
},
|
|
109
|
+
registerPageDir(dirPath) {
|
|
110
|
+
const ctx = this.api?._ctx ?? this._ctx;
|
|
111
|
+
if (!ctx) {
|
|
112
|
+
throw new Error('registerPageDir can only be called during or after onRoutesReady');
|
|
113
|
+
}
|
|
114
|
+
return registerPageDir(dirPath, {
|
|
115
|
+
registry,
|
|
116
|
+
adminPath,
|
|
117
|
+
ctx,
|
|
118
|
+
requireAuth,
|
|
119
|
+
optionalAuth,
|
|
120
|
+
serveAdminPanel,
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
|
|
108
124
|
_ctx: null,
|
|
109
125
|
},
|
|
110
126
|
|
|
@@ -34,9 +34,11 @@ function generateAuditLogComponent(options = {}) {
|
|
|
34
34
|
rows = res.data || [];
|
|
35
35
|
total = (res.meta && res.meta.total) || 0;
|
|
36
36
|
loading = false;
|
|
37
|
+
if (typeof m !== 'undefined' && m.redraw) m.redraw();
|
|
37
38
|
}).catch(function(e) {
|
|
38
39
|
error = e.message || String(e);
|
|
39
40
|
loading = false;
|
|
41
|
+
if (typeof m !== 'undefined' && m.redraw) m.redraw();
|
|
40
42
|
});
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -33,7 +33,8 @@ There are **three** files in this folder; open the reference that matches the ta
|
|
|
33
33
|
| `webspresso dev` | Dev server (watch). |
|
|
34
34
|
| `webspresso db:migrate` / `db:rollback` / `db:status` | ORM migrations. |
|
|
35
35
|
| `webspresso doctor` | Environment / project sanity checks. |
|
|
36
|
-
| `webspresso skill --preset webspresso` | Copies this skill bundle under `.
|
|
36
|
+
| `webspresso skill --preset webspresso` | Copies this skill bundle under `.agents/skills/webspresso-usage/` (`SKILL.md` + `REFERENCE-*.md`). |
|
|
37
|
+
|
|
37
38
|
|
|
38
39
|
## When to use this skill
|
|
39
40
|
|